From cb14dafacac637db5706de1cfe75ff11eaf96f11 Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 2 Sep 2025 15:47:33 +0800 Subject: [PATCH] Feat: Initialize the data pipeline canvas. #9869 (#9870) ### What problem does this PR solve? Feat: Initialize the data pipeline canvas. #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- .../data-flow/canvas/context-menu/index.less | 18 + .../data-flow/canvas/context-menu/index.tsx | 107 + web/src/pages/data-flow/canvas/context.tsx | 56 + web/src/pages/data-flow/canvas/edge/index.tsx | 126 + web/src/pages/data-flow/canvas/index.less | 11 + web/src/pages/data-flow/canvas/index.tsx | 331 +++ .../data-flow/canvas/node/agent-node.tsx | 116 + .../data-flow/canvas/node/begin-node.tsx | 62 + web/src/pages/data-flow/canvas/node/card.tsx | 57 + .../data-flow/canvas/node/categorize-node.tsx | 62 + .../pages/data-flow/canvas/node/dropdown.tsx | 58 + .../node/dropdown/next-step-dropdown.tsx | 295 +++ .../data-flow/canvas/node/email-node.tsx | 80 + .../data-flow/canvas/node/generate-node.tsx | 60 + .../data-flow/canvas/node/handle-icon.tsx | 20 + .../pages/data-flow/canvas/node/handle.tsx | 64 + .../pages/data-flow/canvas/node/index.less | 285 +++ web/src/pages/data-flow/canvas/node/index.tsx | 49 + .../data-flow/canvas/node/invoke-node.tsx | 62 + .../data-flow/canvas/node/iteration-node.tsx | 93 + .../data-flow/canvas/node/keyword-node.tsx | 60 + .../data-flow/canvas/node/logic-node.tsx | 41 + .../data-flow/canvas/node/message-node.tsx | 65 + .../data-flow/canvas/node/node-header.tsx | 34 + .../data-flow/canvas/node/node-wrapper.tsx | 18 + .../data-flow/canvas/node/note-node/index.tsx | 104 + .../canvas/node/note-node/use-watch-change.ts | 30 + .../pages/data-flow/canvas/node/popover.tsx | 121 + .../data-flow/canvas/node/relevant-node.tsx | 73 + .../data-flow/canvas/node/resize-icon.tsx | 32 + .../data-flow/canvas/node/retrieval-node.tsx | 84 + .../data-flow/canvas/node/rewrite-node.tsx | 60 + .../data-flow/canvas/node/switch-node.tsx | 118 + .../data-flow/canvas/node/template-node.tsx | 78 + .../pages/data-flow/canvas/node/tool-node.tsx | 83 + .../pages/data-flow/canvas/node/toolbar.tsx | 88 + .../use-build-categorize-handle-positions.ts | 48 + .../node/use-build-switch-handle-positions.ts | 59 + .../pages/data-flow/components/background.tsx | 13 + web/src/pages/data-flow/constant.tsx | 947 +++++++ web/src/pages/data-flow/context.ts | 50 + .../pages/data-flow/debug-content/index.tsx | 260 ++ .../data-flow/debug-content/popover-form.tsx | 103 + .../data-flow/debug-content/uploader.tsx | 116 + web/src/pages/data-flow/flow-tooltip.tsx | 19 + web/src/pages/data-flow/form-hooks.ts | 43 + .../data-flow/form-sheet/form-config-map.tsx | 165 ++ web/src/pages/data-flow/form-sheet/next.tsx | 134 + .../form-sheet/single-debug-sheet/index.tsx | 89 + .../data-flow/form/agent-form/agent-tools.tsx | 191 ++ .../form/agent-form/dynamic-prompt.tsx | 93 + .../form/agent-form/dynamic-tool.tsx | 63 + .../pages/data-flow/form/agent-form/index.tsx | 280 +++ .../form/agent-form/tool-popover/index.tsx | 89 + .../agent-form/tool-popover/tool-command.tsx | 178 ++ .../agent-form/tool-popover/use-update-mcp.ts | 74 + .../tool-popover/use-update-tools.ts | 66 + .../form/agent-form/use-get-tools.ts | 26 + .../data-flow/form/agent-form/use-values.ts | 33 + .../form/agent-form/use-watch-change.ts | 22 + .../data-flow/form/akshare-form/index.tsx | 22 + .../pages/data-flow/form/arxiv-form/index.tsx | 96 + .../data-flow/form/baidu-fanyi-form/index.tsx | 71 + .../pages/data-flow/form/baidu-form/index.tsx | 22 + .../form/begin-form/begin-dynamic-options.tsx | 57 + .../pages/data-flow/form/begin-form/index.tsx | 205 ++ .../form/begin-form/parameter-dialog.tsx | 226 ++ .../data-flow/form/begin-form/query-table.tsx | 199 ++ .../form/begin-form/use-edit-query.ts | 67 + .../data-flow/form/begin-form/use-values.ts | 34 + .../form/begin-form/use-watch-change.ts | 31 + .../pages/data-flow/form/begin-form/utils.ts | 14 + .../pages/data-flow/form/bing-form/index.tsx | 131 + .../categorize-form/dynamic-categorize.tsx | 249 ++ .../form/categorize-form/dynamic-example.tsx | 68 + .../data-flow/form/categorize-form/index.tsx | 48 + .../form/categorize-form/use-form-schema.ts | 32 + .../form/categorize-form/use-values.ts | 34 + .../form/categorize-form/use-watch-change.ts | 17 + .../pages/data-flow/form/code-form/index.tsx | 168 ++ .../form/code-form/next-variable.tsx | 128 + .../pages/data-flow/form/code-form/schema.ts | 14 + .../data-flow/form/code-form/use-values.ts | 47 + .../form/code-form/use-watch-change.ts | 95 + .../form/components/api-key-field.tsx | 32 + .../form/components/description-field.tsx | 27 + .../components/dynamic-input-variable.tsx | 127 + .../form/components/form-wrapper.tsx | 16 + .../data-flow/form/components/index.less | 22 + .../next-dynamic-input-variable.tsx | 135 + .../data-flow/form/components/output.tsx | 35 + .../form/components/prompt-editor/constant.ts | 1 + .../form/components/prompt-editor/index.css | 76 + .../form/components/prompt-editor/index.tsx | 181 ++ .../prompt-editor/paste-handler-plugin.tsx | 83 + .../form/components/prompt-editor/theme.ts | 43 + .../prompt-editor/variable-node.tsx | 91 + .../variable-on-change-plugin.tsx | 35 + .../prompt-editor/variable-picker-plugin.tsx | 297 +++ .../form/components/query-variable.tsx | 66 + .../data-flow/form/crawler-form/index.tsx | 105 + .../pages/data-flow/form/deepl-form/index.tsx | 36 + .../data-flow/form/duckduckgo-form/index.tsx | 91 + .../pages/data-flow/form/email-form/index.tsx | 161 ++ .../data-flow/form/exesql-form/index.tsx | 167 ++ .../form/exesql-form/use-submit-form.ts | 31 + .../data-flow/form/github-form/index.tsx | 52 + .../data-flow/form/google-form/index.tsx | 139 ++ .../form/google-scholar-form/index.tsx | 166 ++ .../pages/data-flow/form/invoke-form/hooks.ts | 97 + .../data-flow/form/invoke-form/index.tsx | 226 ++ .../data-flow/form/invoke-form/schema.ts | 21 + .../form/invoke-form/use-edit-variable.ts | 70 + .../form/invoke-form/variable-dialog.tsx | 143 ++ .../form/invoke-form/variable-table.tsx | 199 ++ .../form/iteration-form/dynamic-output.tsx | 128 + .../data-flow/form/iteration-form/index.tsx | 57 + .../form/iteration-form/interface.ts | 2 + .../form/iteration-form/use-build-options.ts | 31 + .../form/iteration-form/use-values.ts | 27 + .../iteration-form/use-watch-form-change.ts | 30 + .../form/iteration-start-from/index.tsx | 23 + .../pages/data-flow/form/jin10-form/index.tsx | 145 ++ .../form/keyword-extract-form/index.tsx | 48 + .../data-flow/form/message-form/index.tsx | 101 + .../data-flow/form/message-form/use-values.ts | 22 + .../form/message-form/use-watch-change.ts | 24 + .../data-flow/form/pubmed-form/index.tsx | 90 + .../data-flow/form/qweather-form/index.tsx | 157 ++ .../data-flow/form/relevant-form/hooks.ts | 41 + .../data-flow/form/relevant-form/index.tsx | 49 + .../data-flow/form/retrieval-form/next.tsx | 126 + .../form/retrieval-form/use-values.ts | 25 + .../form/rewrite-question-form/index.tsx | 68 + .../data-flow/form/searxng-form/index.tsx | 73 + .../form/string-transform-form/index.tsx | 166 ++ .../form/string-transform-form/use-values.ts | 33 + .../use-watch-form-change.ts | 26 + .../data-flow/form/switch-form/index.tsx | 328 +++ .../data-flow/form/switch-form/use-values.ts | 17 + .../form/switch-form/use-watch-change.ts | 24 + .../form/tavily-extract-form/index.tsx | 121 + .../form/tavily-form/dynamic-domain.tsx | 60 + .../data-flow/form/tavily-form/index.tsx | 214 ++ .../data-flow/form/tavily-form/use-values.ts | 23 + .../form/tavily-form/use-watch-change.ts | 23 + .../data-flow/form/tushare-form/index.tsx | 83 + .../form/user-fill-up-form/index.tsx | 168 ++ .../form/user-fill-up-form/use-values.ts | 21 + .../user-fill-up-form/use-watch-change.ts | 35 + .../data-flow/form/wencai-form/index.tsx | 97 + .../data-flow/form/wikipedia-form/index.tsx | 88 + .../form/yahoo-finance-form/index.tsx | 125 + web/src/pages/data-flow/hooks.tsx | 405 +++ web/src/pages/data-flow/hooks/use-add-node.ts | 462 ++++ .../hooks/use-agent-tool-initial-values.ts | 70 + .../data-flow/hooks/use-before-delete.tsx | 82 + .../pages/data-flow/hooks/use-build-dsl.ts | 29 + .../data-flow/hooks/use-cache-chat-log.ts | 88 + .../data-flow/hooks/use-change-node-name.ts | 120 + .../pages/data-flow/hooks/use-chat-logic.ts | 60 + .../pages/data-flow/hooks/use-export-json.ts | 71 + .../pages/data-flow/hooks/use-fetch-data.ts | 19 + .../data-flow/hooks/use-find-mcp-by-id.ts | 12 + .../pages/data-flow/hooks/use-form-values.ts | 20 + .../data-flow/hooks/use-get-begin-query.tsx | 317 +++ .../pages/data-flow/hooks/use-iteration.ts | 0 .../pages/data-flow/hooks/use-move-note.ts | 35 + .../data-flow/hooks/use-open-document.ts | 12 + .../pages/data-flow/hooks/use-save-graph.ts | 89 + .../hooks/use-send-shared-message.ts | 79 + .../pages/data-flow/hooks/use-set-graph.ts | 17 + .../pages/data-flow/hooks/use-show-dialog.ts | 91 + .../pages/data-flow/hooks/use-show-drawer.tsx | 186 ++ .../data-flow/hooks/use-watch-form-change.ts | 18 + web/src/pages/data-flow/index.tsx | 187 ++ web/src/pages/data-flow/interface.ts | 43 + web/src/pages/data-flow/operator-icon.tsx | 87 + web/src/pages/data-flow/options.ts | 2174 +++++++++++++++++ web/src/pages/data-flow/run-sheet/index.tsx | 69 + .../pages/data-flow/setting-dialog/index.tsx | 53 + .../data-flow/setting-dialog/setting-form.tsx | 158 ++ web/src/pages/data-flow/share/index.tsx | 223 ++ .../data-flow/share/parameter-dialog.tsx | 30 + web/src/pages/data-flow/store.ts | 534 ++++ .../data-flow/upload-agent-dialog/index.tsx | 36 + .../upload-agent-dialog/upload-agent-form.tsx | 89 + .../data-flow/use-agent-history-manager.ts | 163 ++ web/src/pages/data-flow/utils.ts | 558 +++++ .../data-flow/utils/build-output-list.ts | 8 + web/src/pages/data-flow/utils/chat.ts | 21 + web/src/pages/data-flow/utils/delete-node.ts | 34 + .../utils/filter-downstream-nodes.ts | 63 + .../pages/data-flow/version-dialog/index.tsx | 141 ++ web/src/pages/data-flows/index.tsx | 3 + web/src/routes.ts | 18 + 196 files changed, 21201 insertions(+) create mode 100644 web/src/pages/data-flow/canvas/context-menu/index.less create mode 100644 web/src/pages/data-flow/canvas/context-menu/index.tsx create mode 100644 web/src/pages/data-flow/canvas/context.tsx create mode 100644 web/src/pages/data-flow/canvas/edge/index.tsx create mode 100644 web/src/pages/data-flow/canvas/index.less create mode 100644 web/src/pages/data-flow/canvas/index.tsx create mode 100644 web/src/pages/data-flow/canvas/node/agent-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/begin-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/card.tsx create mode 100644 web/src/pages/data-flow/canvas/node/categorize-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/dropdown.tsx create mode 100644 web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx create mode 100644 web/src/pages/data-flow/canvas/node/email-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/generate-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/handle-icon.tsx create mode 100644 web/src/pages/data-flow/canvas/node/handle.tsx create mode 100644 web/src/pages/data-flow/canvas/node/index.less create mode 100644 web/src/pages/data-flow/canvas/node/index.tsx create mode 100644 web/src/pages/data-flow/canvas/node/invoke-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/iteration-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/keyword-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/logic-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/message-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/node-header.tsx create mode 100644 web/src/pages/data-flow/canvas/node/node-wrapper.tsx create mode 100644 web/src/pages/data-flow/canvas/node/note-node/index.tsx create mode 100644 web/src/pages/data-flow/canvas/node/note-node/use-watch-change.ts create mode 100644 web/src/pages/data-flow/canvas/node/popover.tsx create mode 100644 web/src/pages/data-flow/canvas/node/relevant-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/resize-icon.tsx create mode 100644 web/src/pages/data-flow/canvas/node/retrieval-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/rewrite-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/switch-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/template-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/tool-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/toolbar.tsx create mode 100644 web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts create mode 100644 web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts create mode 100644 web/src/pages/data-flow/components/background.tsx create mode 100644 web/src/pages/data-flow/constant.tsx create mode 100644 web/src/pages/data-flow/context.ts create mode 100644 web/src/pages/data-flow/debug-content/index.tsx create mode 100644 web/src/pages/data-flow/debug-content/popover-form.tsx create mode 100644 web/src/pages/data-flow/debug-content/uploader.tsx create mode 100644 web/src/pages/data-flow/flow-tooltip.tsx create mode 100644 web/src/pages/data-flow/form-hooks.ts create mode 100644 web/src/pages/data-flow/form-sheet/form-config-map.tsx create mode 100644 web/src/pages/data-flow/form-sheet/next.tsx create mode 100644 web/src/pages/data-flow/form-sheet/single-debug-sheet/index.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/agent-tools.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/index.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/index.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/tool-command.tsx create mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/use-update-mcp.ts create mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/use-update-tools.ts create mode 100644 web/src/pages/data-flow/form/agent-form/use-get-tools.ts create mode 100644 web/src/pages/data-flow/form/agent-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/agent-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/akshare-form/index.tsx create mode 100644 web/src/pages/data-flow/form/arxiv-form/index.tsx create mode 100644 web/src/pages/data-flow/form/baidu-fanyi-form/index.tsx create mode 100644 web/src/pages/data-flow/form/baidu-form/index.tsx create mode 100644 web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx create mode 100644 web/src/pages/data-flow/form/begin-form/index.tsx create mode 100644 web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx create mode 100644 web/src/pages/data-flow/form/begin-form/query-table.tsx create mode 100644 web/src/pages/data-flow/form/begin-form/use-edit-query.ts create mode 100644 web/src/pages/data-flow/form/begin-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/begin-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/begin-form/utils.ts create mode 100644 web/src/pages/data-flow/form/bing-form/index.tsx create mode 100644 web/src/pages/data-flow/form/categorize-form/dynamic-categorize.tsx create mode 100644 web/src/pages/data-flow/form/categorize-form/dynamic-example.tsx create mode 100644 web/src/pages/data-flow/form/categorize-form/index.tsx create mode 100644 web/src/pages/data-flow/form/categorize-form/use-form-schema.ts create mode 100644 web/src/pages/data-flow/form/categorize-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/categorize-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/code-form/index.tsx create mode 100644 web/src/pages/data-flow/form/code-form/next-variable.tsx create mode 100644 web/src/pages/data-flow/form/code-form/schema.ts create mode 100644 web/src/pages/data-flow/form/code-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/code-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/components/api-key-field.tsx create mode 100644 web/src/pages/data-flow/form/components/description-field.tsx create mode 100644 web/src/pages/data-flow/form/components/dynamic-input-variable.tsx create mode 100644 web/src/pages/data-flow/form/components/form-wrapper.tsx create mode 100644 web/src/pages/data-flow/form/components/index.less create mode 100644 web/src/pages/data-flow/form/components/next-dynamic-input-variable.tsx create mode 100644 web/src/pages/data-flow/form/components/output.tsx create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/constant.ts create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/index.css create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/index.tsx create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/paste-handler-plugin.tsx create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/theme.ts create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/variable-node.tsx create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/variable-on-change-plugin.tsx create mode 100644 web/src/pages/data-flow/form/components/prompt-editor/variable-picker-plugin.tsx create mode 100644 web/src/pages/data-flow/form/components/query-variable.tsx create mode 100644 web/src/pages/data-flow/form/crawler-form/index.tsx create mode 100644 web/src/pages/data-flow/form/deepl-form/index.tsx create mode 100644 web/src/pages/data-flow/form/duckduckgo-form/index.tsx create mode 100644 web/src/pages/data-flow/form/email-form/index.tsx create mode 100644 web/src/pages/data-flow/form/exesql-form/index.tsx create mode 100644 web/src/pages/data-flow/form/exesql-form/use-submit-form.ts create mode 100644 web/src/pages/data-flow/form/github-form/index.tsx create mode 100644 web/src/pages/data-flow/form/google-form/index.tsx create mode 100644 web/src/pages/data-flow/form/google-scholar-form/index.tsx create mode 100644 web/src/pages/data-flow/form/invoke-form/hooks.ts create mode 100644 web/src/pages/data-flow/form/invoke-form/index.tsx create mode 100644 web/src/pages/data-flow/form/invoke-form/schema.ts create mode 100644 web/src/pages/data-flow/form/invoke-form/use-edit-variable.ts create mode 100644 web/src/pages/data-flow/form/invoke-form/variable-dialog.tsx create mode 100644 web/src/pages/data-flow/form/invoke-form/variable-table.tsx create mode 100644 web/src/pages/data-flow/form/iteration-form/dynamic-output.tsx create mode 100644 web/src/pages/data-flow/form/iteration-form/index.tsx create mode 100644 web/src/pages/data-flow/form/iteration-form/interface.ts create mode 100644 web/src/pages/data-flow/form/iteration-form/use-build-options.ts create mode 100644 web/src/pages/data-flow/form/iteration-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/iteration-form/use-watch-form-change.ts create mode 100644 web/src/pages/data-flow/form/iteration-start-from/index.tsx create mode 100644 web/src/pages/data-flow/form/jin10-form/index.tsx create mode 100644 web/src/pages/data-flow/form/keyword-extract-form/index.tsx create mode 100644 web/src/pages/data-flow/form/message-form/index.tsx create mode 100644 web/src/pages/data-flow/form/message-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/message-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/pubmed-form/index.tsx create mode 100644 web/src/pages/data-flow/form/qweather-form/index.tsx create mode 100644 web/src/pages/data-flow/form/relevant-form/hooks.ts create mode 100644 web/src/pages/data-flow/form/relevant-form/index.tsx create mode 100644 web/src/pages/data-flow/form/retrieval-form/next.tsx create mode 100644 web/src/pages/data-flow/form/retrieval-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/rewrite-question-form/index.tsx create mode 100644 web/src/pages/data-flow/form/searxng-form/index.tsx create mode 100644 web/src/pages/data-flow/form/string-transform-form/index.tsx create mode 100644 web/src/pages/data-flow/form/string-transform-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/string-transform-form/use-watch-form-change.ts create mode 100644 web/src/pages/data-flow/form/switch-form/index.tsx create mode 100644 web/src/pages/data-flow/form/switch-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/switch-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/tavily-extract-form/index.tsx create mode 100644 web/src/pages/data-flow/form/tavily-form/dynamic-domain.tsx create mode 100644 web/src/pages/data-flow/form/tavily-form/index.tsx create mode 100644 web/src/pages/data-flow/form/tavily-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/tavily-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/tushare-form/index.tsx create mode 100644 web/src/pages/data-flow/form/user-fill-up-form/index.tsx create mode 100644 web/src/pages/data-flow/form/user-fill-up-form/use-values.ts create mode 100644 web/src/pages/data-flow/form/user-fill-up-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/wencai-form/index.tsx create mode 100644 web/src/pages/data-flow/form/wikipedia-form/index.tsx create mode 100644 web/src/pages/data-flow/form/yahoo-finance-form/index.tsx create mode 100644 web/src/pages/data-flow/hooks.tsx create mode 100644 web/src/pages/data-flow/hooks/use-add-node.ts create mode 100644 web/src/pages/data-flow/hooks/use-agent-tool-initial-values.ts create mode 100644 web/src/pages/data-flow/hooks/use-before-delete.tsx create mode 100644 web/src/pages/data-flow/hooks/use-build-dsl.ts create mode 100644 web/src/pages/data-flow/hooks/use-cache-chat-log.ts create mode 100644 web/src/pages/data-flow/hooks/use-change-node-name.ts create mode 100644 web/src/pages/data-flow/hooks/use-chat-logic.ts create mode 100644 web/src/pages/data-flow/hooks/use-export-json.ts create mode 100644 web/src/pages/data-flow/hooks/use-fetch-data.ts create mode 100644 web/src/pages/data-flow/hooks/use-find-mcp-by-id.ts create mode 100644 web/src/pages/data-flow/hooks/use-form-values.ts create mode 100644 web/src/pages/data-flow/hooks/use-get-begin-query.tsx create mode 100644 web/src/pages/data-flow/hooks/use-iteration.ts create mode 100644 web/src/pages/data-flow/hooks/use-move-note.ts create mode 100644 web/src/pages/data-flow/hooks/use-open-document.ts create mode 100644 web/src/pages/data-flow/hooks/use-save-graph.ts create mode 100644 web/src/pages/data-flow/hooks/use-send-shared-message.ts create mode 100644 web/src/pages/data-flow/hooks/use-set-graph.ts create mode 100644 web/src/pages/data-flow/hooks/use-show-dialog.ts create mode 100644 web/src/pages/data-flow/hooks/use-show-drawer.tsx create mode 100644 web/src/pages/data-flow/hooks/use-watch-form-change.ts create mode 100644 web/src/pages/data-flow/index.tsx create mode 100644 web/src/pages/data-flow/interface.ts create mode 100644 web/src/pages/data-flow/operator-icon.tsx create mode 100644 web/src/pages/data-flow/options.ts create mode 100644 web/src/pages/data-flow/run-sheet/index.tsx create mode 100644 web/src/pages/data-flow/setting-dialog/index.tsx create mode 100644 web/src/pages/data-flow/setting-dialog/setting-form.tsx create mode 100644 web/src/pages/data-flow/share/index.tsx create mode 100644 web/src/pages/data-flow/share/parameter-dialog.tsx create mode 100644 web/src/pages/data-flow/store.ts create mode 100644 web/src/pages/data-flow/upload-agent-dialog/index.tsx create mode 100644 web/src/pages/data-flow/upload-agent-dialog/upload-agent-form.tsx create mode 100644 web/src/pages/data-flow/use-agent-history-manager.ts create mode 100644 web/src/pages/data-flow/utils.ts create mode 100644 web/src/pages/data-flow/utils/build-output-list.ts create mode 100644 web/src/pages/data-flow/utils/chat.ts create mode 100644 web/src/pages/data-flow/utils/delete-node.ts create mode 100644 web/src/pages/data-flow/utils/filter-downstream-nodes.ts create mode 100644 web/src/pages/data-flow/version-dialog/index.tsx create mode 100644 web/src/pages/data-flows/index.tsx diff --git a/web/src/pages/data-flow/canvas/context-menu/index.less b/web/src/pages/data-flow/canvas/context-menu/index.less new file mode 100644 index 000000000..5594aa912 --- /dev/null +++ b/web/src/pages/data-flow/canvas/context-menu/index.less @@ -0,0 +1,18 @@ +.contextMenu { + background: rgba(255, 255, 255, 0.1); + border-style: solid; + box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%); + position: absolute; + z-index: 10; + button { + border: none; + display: block; + padding: 0.5em; + text-align: left; + width: 100%; + } + + button:hover { + background: rgba(255, 255, 255, 0.1); + } +} diff --git a/web/src/pages/data-flow/canvas/context-menu/index.tsx b/web/src/pages/data-flow/canvas/context-menu/index.tsx new file mode 100644 index 000000000..6cb306af9 --- /dev/null +++ b/web/src/pages/data-flow/canvas/context-menu/index.tsx @@ -0,0 +1,107 @@ +import { NodeMouseHandler, useReactFlow } from '@xyflow/react'; +import { useCallback, useRef, useState } from 'react'; + +import styles from './index.less'; + +export interface INodeContextMenu { + id: string; + top: number; + left: number; + right?: number; + bottom?: number; + [key: string]: unknown; +} + +export function NodeContextMenu({ + id, + top, + left, + right, + bottom, + ...props +}: INodeContextMenu) { + const { getNode, setNodes, addNodes, setEdges } = useReactFlow(); + + const duplicateNode = useCallback(() => { + const node = getNode(id); + const position = { + x: node?.position?.x || 0 + 50, + y: node?.position?.y || 0 + 50, + }; + + addNodes({ + ...(node || {}), + data: node?.data, + selected: false, + dragging: false, + id: `${node?.id}-copy`, + position, + }); + }, [id, getNode, addNodes]); + + const deleteNode = useCallback(() => { + setNodes((nodes) => nodes.filter((node) => node.id !== id)); + setEdges((edges) => edges.filter((edge) => edge.source !== id)); + }, [id, setNodes, setEdges]); + + return ( +
+

+ node: {id} +

+ + +
+ ); +} + +/* @deprecated + */ +export const useHandleNodeContextMenu = (sideWidth: number) => { + const [menu, setMenu] = useState({} as INodeContextMenu); + const ref = useRef(null); + + const onNodeContextMenu: NodeMouseHandler = useCallback( + (event, node) => { + // Prevent native context menu from showing + event.preventDefault(); + + // Calculate position of the context menu. We want to make sure it + // doesn't get positioned off-screen. + const pane = ref.current?.getBoundingClientRect(); + // setMenu({ + // id: node.id, + // top: event.clientY < pane.height - 200 ? event.clientY : 0, + // left: event.clientX < pane.width - 200 ? event.clientX : 0, + // right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0, + // bottom: + // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0, + // }); + + setMenu({ + id: node.id, + top: event.clientY - 144, + left: event.clientX - sideWidth, + // top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0, + // left: event.clientX < pane.width - 200 ? event.clientX : 0, + }); + }, + [sideWidth], + ); + + // Close the context menu if it's open whenever the window is clicked. + const onPaneClick = useCallback( + () => setMenu({} as INodeContextMenu), + [setMenu], + ); + + return { onNodeContextMenu, menu, onPaneClick, ref }; +}; diff --git a/web/src/pages/data-flow/canvas/context.tsx b/web/src/pages/data-flow/canvas/context.tsx new file mode 100644 index 000000000..203c37e99 --- /dev/null +++ b/web/src/pages/data-flow/canvas/context.tsx @@ -0,0 +1,56 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useRef, +} from 'react'; + +interface DropdownContextType { + canShowDropdown: () => boolean; + setActiveDropdown: (type: 'handle' | 'drag') => void; + clearActiveDropdown: () => void; +} + +const DropdownContext = createContext(null); + +export const useDropdownManager = () => { + const context = useContext(DropdownContext); + if (!context) { + throw new Error('useDropdownManager must be used within DropdownProvider'); + } + return context; +}; + +interface DropdownProviderProps { + children: ReactNode; +} + +export const DropdownProvider = ({ children }: DropdownProviderProps) => { + const activeDropdownRef = useRef<'handle' | 'drag' | null>(null); + + const canShowDropdown = useCallback(() => { + const current = activeDropdownRef.current; + return !current; + }, []); + + const setActiveDropdown = useCallback((type: 'handle' | 'drag') => { + activeDropdownRef.current = type; + }, []); + + const clearActiveDropdown = useCallback(() => { + activeDropdownRef.current = null; + }, []); + + const value: DropdownContextType = { + canShowDropdown, + setActiveDropdown, + clearActiveDropdown, + }; + + return ( + + {children} + + ); +}; diff --git a/web/src/pages/data-flow/canvas/edge/index.tsx b/web/src/pages/data-flow/canvas/edge/index.tsx new file mode 100644 index 000000000..3e1b57e85 --- /dev/null +++ b/web/src/pages/data-flow/canvas/edge/index.tsx @@ -0,0 +1,126 @@ +import { + BaseEdge, + Edge, + EdgeLabelRenderer, + EdgeProps, + getBezierPath, +} from '@xyflow/react'; +import { memo } from 'react'; +import useGraphStore from '../../store'; + +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { cn } from '@/lib/utils'; +import { useMemo } from 'react'; +import { NodeHandleId, Operator } from '../../constant'; + +function InnerButtonEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + source, + target, + style = {}, + markerEnd, + selected, + data, + sourceHandleId, +}: EdgeProps>) { + const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById); + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + const selectedStyle = useMemo(() => { + return selected ? { strokeWidth: 1, stroke: 'rgba(76, 164, 231, 1)' } : {}; + }, [selected]); + + const onEdgeClick = () => { + deleteEdgeById(id); + }; + + // highlight the nodes that the workflow passes through + const { data: flowDetail } = useFetchAgent(); + + const graphPath = useMemo(() => { + // TODO: this will be called multiple times + const path = flowDetail?.dsl?.path ?? []; + // The second to last + const previousGraphPath: string[] = path.at(-2) ?? []; + let graphPath: string[] = path.at(-1) ?? []; + // The last of the second to last article + const previousLatestElement = previousGraphPath.at(-1); + if (previousGraphPath.length > 0 && previousLatestElement) { + graphPath = [previousLatestElement, ...graphPath]; + } + return Array.isArray(graphPath) ? graphPath : []; + }, [flowDetail.dsl?.path]); + + const highlightStyle = useMemo(() => { + const idx = graphPath.findIndex((x) => x === source); + if (idx !== -1) { + // The set of elements following source + const slicedGraphPath = graphPath.slice(idx + 1); + if (slicedGraphPath.some((x) => x === target)) { + return { strokeWidth: 1, stroke: 'red' }; + } + } + return {}; + }, [source, target, graphPath]); + + const visible = useMemo(() => { + return ( + data?.isHovered && + sourceHandleId !== NodeHandleId.Tool && + sourceHandleId !== NodeHandleId.AgentBottom && // The connection between the agent node and the tool node does not need to display the delete button + !target.startsWith(Operator.Tool) + ); + }, [data?.isHovered, sourceHandleId, target]); + + return ( + <> + + + +
+ +
+
+ + ); +} + +export const ButtonEdge = memo(InnerButtonEdge); diff --git a/web/src/pages/data-flow/canvas/index.less b/web/src/pages/data-flow/canvas/index.less new file mode 100644 index 000000000..0183d41b5 --- /dev/null +++ b/web/src/pages/data-flow/canvas/index.less @@ -0,0 +1,11 @@ +.canvasWrapper { + position: relative; + height: calc(100% - 64px); + :global(.react-flow__node-group) { + .commonNode(); + border-radius: 0 0 10px 10px; + padding: 0; + border: 0; + background-color: transparent; + } +} diff --git a/web/src/pages/data-flow/canvas/index.tsx b/web/src/pages/data-flow/canvas/index.tsx new file mode 100644 index 000000000..eb91ee584 --- /dev/null +++ b/web/src/pages/data-flow/canvas/index.tsx @@ -0,0 +1,331 @@ +import { useIsDarkTheme, useTheme } from '@/components/theme-provider'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { cn } from '@/lib/utils'; +import { + Connection, + ConnectionMode, + ControlButton, + Controls, + NodeTypes, + Position, + ReactFlow, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { NotebookPen } from 'lucide-react'; +import { useCallback, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AgentBackground } from '../components/background'; +import { AgentInstanceContext, HandleContext } from '../context'; + +import FormSheet from '../form-sheet/next'; +import { + useHandleDrop, + useSelectCanvasData, + useValidateConnection, +} from '../hooks'; +import { useAddNode } from '../hooks/use-add-node'; +import { useBeforeDelete } from '../hooks/use-before-delete'; +import { useMoveNote } from '../hooks/use-move-note'; +import { useDropdownManager } from './context'; + +import { + useHideFormSheetOnNodeDeletion, + useShowDrawer, +} from '../hooks/use-show-drawer'; +import RunSheet from '../run-sheet'; +import { ButtonEdge } from './edge'; +import styles from './index.less'; +import { RagNode } from './node'; +import { AgentNode } from './node/agent-node'; +import { BeginNode } from './node/begin-node'; +import { CategorizeNode } from './node/categorize-node'; +import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown'; +import { GenerateNode } from './node/generate-node'; +import { InvokeNode } from './node/invoke-node'; +import { IterationNode, IterationStartNode } from './node/iteration-node'; +import { KeywordNode } from './node/keyword-node'; +import { LogicNode } from './node/logic-node'; +import { MessageNode } from './node/message-node'; +import NoteNode from './node/note-node'; +import { RelevantNode } from './node/relevant-node'; +import { RetrievalNode } from './node/retrieval-node'; +import { RewriteNode } from './node/rewrite-node'; +import { SwitchNode } from './node/switch-node'; +import { TemplateNode } from './node/template-node'; +import { ToolNode } from './node/tool-node'; + +export const nodeTypes: NodeTypes = { + ragNode: RagNode, + categorizeNode: CategorizeNode, + beginNode: BeginNode, + relevantNode: RelevantNode, + logicNode: LogicNode, + noteNode: NoteNode, + switchNode: SwitchNode, + generateNode: GenerateNode, + retrievalNode: RetrievalNode, + messageNode: MessageNode, + rewriteNode: RewriteNode, + keywordNode: KeywordNode, + invokeNode: InvokeNode, + templateNode: TemplateNode, + // emailNode: EmailNode, + group: IterationNode, + iterationStartNode: IterationStartNode, + agentNode: AgentNode, + toolNode: ToolNode, +}; + +const edgeTypes = { + buttonEdge: ButtonEdge, +}; + +interface IProps { + drawerVisible: boolean; + hideDrawer(): void; +} + +function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { + const { t } = useTranslation(); + const { + nodes, + edges, + onConnect: originalOnConnect, + onEdgesChange, + onNodesChange, + onSelectionChange, + onEdgeMouseEnter, + onEdgeMouseLeave, + } = useSelectCanvasData(); + const isValidConnection = useValidateConnection(); + + const { onDrop, onDragOver, setReactFlowInstance, reactFlowInstance } = + useHandleDrop(); + + const { + onNodeClick, + clickedNode, + formDrawerVisible, + hideFormDrawer, + singleDebugDrawerVisible, + hideSingleDebugDrawer, + showSingleDebugDrawer, + chatVisible, + runVisible, + hideRunOrChatDrawer, + showChatModal, + showFormDrawer, + } = useShowDrawer({ + drawerVisible, + hideDrawer, + }); + + const { handleBeforeDelete } = useBeforeDelete(); + + const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance); + + const { ref, showImage, hideImage, imgVisible, mouse } = useMoveNote(); + + const { theme } = useTheme(); + + const isDarkTheme = useIsDarkTheme(); + + useHideFormSheetOnNodeDeletion({ hideFormDrawer }); + + const { visible, hideModal, showModal } = useSetModalState(); + const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 }); + + const isConnectedRef = useRef(false); + const connectionStartRef = useRef<{ + nodeId: string; + handleId: string; + } | null>(null); + + const preventCloseRef = useRef(false); + + const { setActiveDropdown, clearActiveDropdown } = useDropdownManager(); + + const onPaneClick = useCallback(() => { + hideFormDrawer(); + if (visible && !preventCloseRef.current) { + hideModal(); + clearActiveDropdown(); + } + if (imgVisible) { + addNoteNode(mouse); + hideImage(); + } + }, [ + hideFormDrawer, + visible, + hideModal, + imgVisible, + addNoteNode, + mouse, + hideImage, + clearActiveDropdown, + ]); + + const onConnect = (connection: Connection) => { + originalOnConnect(connection); + isConnectedRef.current = true; + }; + + const OnConnectStart = (event: any, params: any) => { + isConnectedRef.current = false; + + if (params && params.nodeId && params.handleId) { + connectionStartRef.current = { + nodeId: params.nodeId, + handleId: params.handleId, + }; + } else { + connectionStartRef.current = null; + } + }; + + const OnConnectEnd = (event: MouseEvent | TouchEvent) => { + if ('clientX' in event && 'clientY' in event) { + const { clientX, clientY } = event; + setDropdownPosition({ x: clientX, y: clientY }); + if (!isConnectedRef.current) { + setActiveDropdown('drag'); + showModal(); + preventCloseRef.current = true; + setTimeout(() => { + preventCloseRef.current = false; + }, 300); + } + } + }; + + return ( +
+ + + + + + + + + + + + + + + {t('flow.note')} + + + + + {visible && ( + + { + hideModal(); + clearActiveDropdown(); + }} + position={dropdownPosition} + > + + + + )} + + + {formDrawerVisible && ( + + + + )} + {runVisible && ( + + )} +
+ ); +} + +export default AgentCanvas; diff --git a/web/src/pages/data-flow/canvas/node/agent-node.tsx b/web/src/pages/data-flow/canvas/node/agent-node.tsx new file mode 100644 index 000000000..42b489a41 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/agent-node.tsx @@ -0,0 +1,116 @@ +import LLMLabel from '@/components/llm-select/llm-label'; +import { IAgentNode } from '@/interfaces/database/flow'; +import { Handle, NodeProps, Position } from '@xyflow/react'; +import { get } from 'lodash'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AgentExceptionMethod, NodeHandleId } from '../../constant'; +import useGraphStore from '../../store'; +import { isBottomSubAgent } from '../../utils'; +import { CommonHandle } from './handle'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; + +function InnerAgentNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const edges = useGraphStore((state) => state.edges); + const { t } = useTranslation(); + + const isHeadAgent = useMemo(() => { + return !isBottomSubAgent(edges, id); + }, [edges, id]); + + const exceptionMethod = useMemo(() => { + return get(data, 'form.exception_method'); + }, [data]); + + const isGotoMethod = useMemo(() => { + return exceptionMethod === AgentExceptionMethod.Goto; + }, [exceptionMethod]); + + return ( + + + {isHeadAgent && ( + <> + + + + )} + + + + + +
+
+ +
+ {(isGotoMethod || + exceptionMethod === AgentExceptionMethod.Comment) && ( +
+ {t('flow.onFailure')} + + {t(`flow.${exceptionMethod}`)} + +
+ )} +
+ {isGotoMethod && ( + + )} +
+
+ ); +} + +export const AgentNode = memo(InnerAgentNode); diff --git a/web/src/pages/data-flow/canvas/node/begin-node.tsx b/web/src/pages/data-flow/canvas/node/begin-node.tsx new file mode 100644 index 000000000..be80d56ae --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/begin-node.tsx @@ -0,0 +1,62 @@ +import { IBeginNode } from '@/interfaces/database/flow'; +import { cn } from '@/lib/utils'; +import { NodeProps, Position } from '@xyflow/react'; +import get from 'lodash/get'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + BeginQueryType, + BeginQueryTypeIconMap, + NodeHandleId, + Operator, +} from '../../constant'; +import { BeginQuery } from '../../interface'; +import OperatorIcon from '../../operator-icon'; +import { CommonHandle } from './handle'; +import { RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import { NodeWrapper } from './node-wrapper'; + +// TODO: do not allow other nodes to connect to this node +function InnerBeginNode({ data, id, selected }: NodeProps) { + const { t } = useTranslation(); + const inputs: Record = get(data, 'form.inputs', {}); + + return ( + + + +
+ +
+ {t(`flow.begin`)} +
+
+
+ {Object.entries(inputs).map(([key, val], idx) => { + const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType]; + return ( +
+ + + {val.name} + {val.optional ? 'Yes' : 'No'} +
+ ); + })} +
+
+ ); +} + +export const BeginNode = memo(InnerBeginNode); diff --git a/web/src/pages/data-flow/canvas/node/card.tsx b/web/src/pages/data-flow/canvas/node/card.tsx new file mode 100644 index 000000000..042ca45e0 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/card.tsx @@ -0,0 +1,57 @@ +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +export function CardWithForm() { + return ( + + + Create project + Deploy your new project in one-click. + + +
+
+
+ + +
+
+ + +
+
+
+
+ + + + +
+ ); +} diff --git a/web/src/pages/data-flow/canvas/node/categorize-node.tsx b/web/src/pages/data-flow/canvas/node/categorize-node.tsx new file mode 100644 index 000000000..a54136b5a --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/categorize-node.tsx @@ -0,0 +1,62 @@ +import LLMLabel from '@/components/llm-select/llm-label'; +import { ICategorizeNode } from '@/interfaces/database/flow'; +import { NodeProps, Position } from '@xyflow/react'; +import { get } from 'lodash'; +import { memo } from 'react'; +import { NodeHandleId } from '../../constant'; +import { CommonHandle } from './handle'; +import { RightHandleStyle } from './handle-icon'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; +import { useBuildCategorizeHandlePositions } from './use-build-categorize-handle-positions'; + +export function InnerCategorizeNode({ + id, + data, + selected, +}: NodeProps) { + const { positions } = useBuildCategorizeHandlePositions({ data, id }); + return ( + + + + + + +
+
+ +
+ {positions.map((position) => { + return ( +
+
+ {position.name} +
+ +
+ ); + })} +
+
+
+ ); +} + +export const CategorizeNode = memo(InnerCategorizeNode); diff --git a/web/src/pages/data-flow/canvas/node/dropdown.tsx b/web/src/pages/data-flow/canvas/node/dropdown.tsx new file mode 100644 index 000000000..dd5263abc --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/dropdown.tsx @@ -0,0 +1,58 @@ +import OperateDropdown from '@/components/operate-dropdown'; +import { CopyOutlined } from '@ant-design/icons'; +import { Flex, MenuProps } from 'antd'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Operator } from '../../constant'; +import { useDuplicateNode } from '../../hooks'; +import useGraphStore from '../../store'; + +interface IProps { + id: string; + iconFontColor?: string; + label: string; +} + +const NodeDropdown = ({ id, iconFontColor, label }: IProps) => { + const { t } = useTranslation(); + const deleteNodeById = useGraphStore((store) => store.deleteNodeById); + const deleteIterationNodeById = useGraphStore( + (store) => store.deleteIterationNodeById, + ); + + const deleteNode = useCallback(() => { + if (label === Operator.Iteration) { + deleteIterationNodeById(id); + } else { + deleteNodeById(id); + } + }, [label, deleteIterationNodeById, id, deleteNodeById]); + + const duplicateNode = useDuplicateNode(); + + const items: MenuProps['items'] = [ + { + key: '2', + onClick: () => duplicateNode(id, label), + label: ( + + {t('common.copy')} + + + ), + }, + ]; + + return ( + + ); +}; + +export default NodeDropdown; diff --git a/web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx b/web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx new file mode 100644 index 000000000..6d9f32453 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx @@ -0,0 +1,295 @@ +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { IModalProps } from '@/interfaces/common'; +import { Operator } from '@/pages/agent/constant'; +import { AgentInstanceContext, HandleContext } from '@/pages/agent/context'; +import OperatorIcon from '@/pages/agent/operator-icon'; +import { Position } from '@xyflow/react'; +import { t } from 'i18next'; +import { lowerFirst } from 'lodash'; +import { + PropsWithChildren, + createContext, + memo, + useContext, + useEffect, + useRef, +} from 'react'; +import { useTranslation } from 'react-i18next'; + +type OperatorItemProps = { + operators: Operator[]; + isCustomDropdown?: boolean; + mousePosition?: { x: number; y: number }; +}; + +const HideModalContext = createContext['showModal']>(() => {}); +const OnNodeCreatedContext = createContext< + ((newNodeId: string) => void) | undefined +>(undefined); + +function OperatorItemList({ + operators, + isCustomDropdown = false, + mousePosition, +}: OperatorItemProps) { + const { addCanvasNode } = useContext(AgentInstanceContext); + const handleContext = useContext(HandleContext); + const hideModal = useContext(HideModalContext); + const onNodeCreated = useContext(OnNodeCreatedContext); + const { t } = useTranslation(); + + const handleClick = (operator: Operator) => { + const contextData = handleContext || { + nodeId: '', + id: '', + type: 'source' as const, + position: Position.Right, + isFromConnectionDrag: true, + }; + + const mockEvent = mousePosition + ? { + clientX: mousePosition.x, + clientY: mousePosition.y, + } + : undefined; + + const newNodeId = addCanvasNode(operator, contextData)(mockEvent); + + if (onNodeCreated && newNodeId) { + onNodeCreated(newNodeId); + } + + hideModal?.(); + }; + + const renderOperatorItem = (operator: Operator) => { + const commonContent = ( +
+ + {t(`flow.${lowerFirst(operator)}`)} +
+ ); + + return ( + + + {isCustomDropdown ? ( +
  • handleClick(operator)}>{commonContent}
  • + ) : ( + handleClick(operator)} + onSelect={() => hideModal?.()} + > + + {t(`flow.${lowerFirst(operator)}`)} + + )} +
    + +

    {t(`flow.${lowerFirst(operator)}Description`)}

    +
    +
    + ); + }; + + return
      {operators.map(renderOperatorItem)}
    ; +} + +function AccordionOperators({ + isCustomDropdown = false, + mousePosition, +}: { + isCustomDropdown?: boolean; + mousePosition?: { x: number; y: number }; +}) { + return ( + + + + {t('flow.foundation')} + + + + + + + + {t('flow.dialog')} + + + + + + + + {t('flow.flow')} + + + + + + + + {t('flow.dataManipulation')} + + + + + + + + {t('flow.tools')} + + + + + + + ); +} + +export function InnerNextStepDropdown({ + children, + hideModal, + position, + onNodeCreated, +}: PropsWithChildren & + IModalProps & { + position?: { x: number; y: number }; + onNodeCreated?: (newNodeId: string) => void; + }) { + const dropdownRef = useRef(null); + + useEffect(() => { + if (position && hideModal) { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + hideModal(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + } + }, [position, hideModal]); + + if (position) { + return ( +
    e.stopPropagation()} + > +
    +
    +
    {t('flow.nextStep')}
    +
    + + + + + +
    +
    + ); + } + + return ( + { + if (!open && hideModal) { + hideModal(); + } + }} + > + {children} + e.stopPropagation()} + className="w-[300px] font-semibold" + > + {t('flow.nextStep')} + + + + + + ); +} + +export const NextStepDropdown = memo(InnerNextStepDropdown); diff --git a/web/src/pages/data-flow/canvas/node/email-node.tsx b/web/src/pages/data-flow/canvas/node/email-node.tsx new file mode 100644 index 000000000..9482194f3 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/email-node.tsx @@ -0,0 +1,80 @@ +import { IEmailNode } from '@/interfaces/database/flow'; +import { Handle, NodeProps, Position } from '@xyflow/react'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { memo, useState } from 'react'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; + +export function InnerEmailNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const [showDetails, setShowDetails] = useState(false); + + return ( +
    + + + + + +
    setShowDetails(!showDetails)} + > +
    + SMTP: + {data.form?.smtp_server} +
    +
    + Port: + {data.form?.smtp_port} +
    +
    + From: + {data.form?.email} +
    +
    {showDetails ? '▼' : '▶'}
    +
    + + {showDetails && ( +
    +
    Expected Input JSON:
    +
    +              {`{
    +  "to_email": "...",
    +  "cc_email": "...", 
    +  "subject": "...",
    +  "content": "..."
    +}`}
    +            
    +
    + )} +
    +
    + ); +} + +export const EmailNode = memo(InnerEmailNode); diff --git a/web/src/pages/data-flow/canvas/node/generate-node.tsx b/web/src/pages/data-flow/canvas/node/generate-node.tsx new file mode 100644 index 000000000..8ffbbd79c --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/generate-node.tsx @@ -0,0 +1,60 @@ +import LLMLabel from '@/components/llm-select/llm-label'; +import { useTheme } from '@/components/theme-provider'; +import { IGenerateNode } from '@/interfaces/database/flow'; +import { Handle, NodeProps, Position } from '@xyflow/react'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { memo } from 'react'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; + +export function InnerGenerateNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const { theme } = useTheme(); + return ( +
    + + + + + +
    + +
    +
    + ); +} + +export const GenerateNode = memo(InnerGenerateNode); diff --git a/web/src/pages/data-flow/canvas/node/handle-icon.tsx b/web/src/pages/data-flow/canvas/node/handle-icon.tsx new file mode 100644 index 000000000..36c7f3634 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/handle-icon.tsx @@ -0,0 +1,20 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { CSSProperties } from 'react'; + +export const HandleIcon = () => { + return ( + + ); +}; + +export const RightHandleStyle: CSSProperties = { + right: 0, +}; + +export const LeftHandleStyle: CSSProperties = { + left: 0, +}; + +export default HandleIcon; diff --git a/web/src/pages/data-flow/canvas/node/handle.tsx b/web/src/pages/data-flow/canvas/node/handle.tsx new file mode 100644 index 000000000..71b473cc7 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/handle.tsx @@ -0,0 +1,64 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import { cn } from '@/lib/utils'; +import { Handle, HandleProps } from '@xyflow/react'; +import { Plus } from 'lucide-react'; +import { useMemo } from 'react'; +import { HandleContext } from '../../context'; +import { useDropdownManager } from '../context'; +import { InnerNextStepDropdown } from './dropdown/next-step-dropdown'; + +export function CommonHandle({ + className, + nodeId, + ...props +}: HandleProps & { nodeId: string }) { + const { visible, hideModal, showModal } = useSetModalState(); + + const { canShowDropdown, setActiveDropdown, clearActiveDropdown } = + useDropdownManager(); + + const value = useMemo( + () => ({ + nodeId, + id: props.id || undefined, + type: props.type, + position: props.position, + isFromConnectionDrag: false, + }), + [nodeId, props.id, props.position, props.type], + ); + + return ( + + { + e.stopPropagation(); + + if (!canShowDropdown()) { + return; + } + + setActiveDropdown('handle'); + showModal(); + }} + > + + {visible && ( + { + hideModal(); + clearActiveDropdown(); + }} + > + + + )} + + + ); +} diff --git a/web/src/pages/data-flow/canvas/node/index.less b/web/src/pages/data-flow/canvas/node/index.less new file mode 100644 index 000000000..14d7e6077 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/index.less @@ -0,0 +1,285 @@ +.dark { + background: rgb(63, 63, 63) !important; +} +.ragNode { + .commonNode(); + .nodeName { + font-size: 10px; + color: black; + } + label { + display: block; + color: #777; + font-size: 12px; + } + .description { + font-size: 10px; + } + + .categorizeAnchorPointText { + position: absolute; + top: -4px; + left: 8px; + white-space: nowrap; + } +} + +@lightBackgroundColor: rgba(150, 150, 150, 0.1); +@darkBackgroundColor: rgba(150, 150, 150, 0.2); + +.selectedNode { + border: 1.5px solid rgb(59, 118, 244); +} + +.selectedIterationNode { + border-bottom: 1.5px solid rgb(59, 118, 244); + border-left: 1.5px solid rgb(59, 118, 244); + border-right: 1.5px solid rgb(59, 118, 244); +} + +.iterationHeader { + .commonNodeShadow(); +} + +.selectedHeader { + border-top: 1.9px solid rgb(59, 118, 244); + border-left: 1.9px solid rgb(59, 118, 244); + border-right: 1.9px solid rgb(59, 118, 244); +} + +.handle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + background: rgb(59, 88, 253); + border: 1px solid white; + z-index: 1; + background-image: url('@/assets/svg/plus.svg'); + background-size: cover; + background-position: center; +} + +.jsonView { + word-wrap: break-word; + overflow: auto; + max-width: 300px; + max-height: 500px; +} + +.logicNode { + .commonNode(); + + .nodeName { + font-size: 10px; + color: black; + } + label { + display: block; + color: #777; + font-size: 12px; + } + + .description { + font-size: 10px; + } + + .categorizeAnchorPointText { + position: absolute; + top: -4px; + left: 8px; + white-space: nowrap; + } + .relevantSourceLabel { + font-size: 10px; + } +} + +.noteNode { + .commonNode(); + min-width: 140px; + width: auto; + height: 100%; + padding: 8px; + border-radius: 10px; + min-height: 128px; + .noteTitle { + background-color: #edfcff; + font-size: 12px; + padding: 6px 6px 4px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + .noteTitleDark { + background-color: #edfcff; + font-size: 12px; + padding: 6px 6px 4px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + .noteForm { + margin-top: 4px; + height: calc(100% - 50px); + } + .noteName { + padding: 0px 4px; + } + .noteTextarea { + resize: none; + border: 0; + border-radius: 0; + height: 100%; + &:focus { + border: none; + box-shadow: none; + } + } +} + +.iterationNode { + .commonNodeShadow(); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; +} + +.nodeText { + padding-inline: 0.4em; + padding-block: 0.2em 0.1em; + background: @lightBackgroundColor; + border-radius: 3px; + min-height: 22px; + .textEllipsis(); +} + +.nodeHeader { + padding-bottom: 12px; +} + +.zeroDivider { + margin: 0 !important; +} + +.conditionBlock { + border-radius: 4px; + padding: 6px; + background: @lightBackgroundColor; +} + +.conditionLine { + border-radius: 4px; + padding: 0 4px; + background: @darkBackgroundColor; + .textEllipsis(); +} + +.conditionKey { + flex: 1; +} + +.conditionOperator { + padding: 0 2px; + text-align: center; +} + +.relevantLabel { + text-align: right; +} + +.knowledgeNodeName { + .textEllipsis(); +} + +.messageNodeContainer { + overflow-y: auto; + max-height: 300px; +} + +.generateParameters { + padding-top: 8px; + label { + flex: 2; + .textEllipsis(); + } + .parameterValue { + flex: 3; + .conditionLine; + } +} + +.emailNodeContainer { + padding: 8px; + font-size: 12px; + + .emailConfig { + background: rgba(0, 0, 0, 0.02); + border-radius: 4px; + padding: 8px; + position: relative; + cursor: pointer; + + &:hover { + background: rgba(0, 0, 0, 0.04); + } + + .configItem { + display: flex; + align-items: center; + margin-bottom: 4px; + + &:last-child { + margin-bottom: 0; + } + + .configLabel { + color: #666; + width: 45px; + flex-shrink: 0; + } + + .configValue { + color: #333; + word-break: break-all; + } + } + + .expandIcon { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + color: #666; + font-size: 12px; + } + } + + .jsonExample { + background: #f5f5f5; + border-radius: 4px; + padding: 8px; + margin-top: 4px; + animation: slideDown 0.2s ease-out; + + .jsonTitle { + color: #666; + margin-bottom: 4px; + } + + .jsonContent { + margin: 0; + color: #333; + font-family: monospace; + } + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/web/src/pages/data-flow/canvas/node/index.tsx b/web/src/pages/data-flow/canvas/node/index.tsx new file mode 100644 index 000000000..a1d48955b --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/index.tsx @@ -0,0 +1,49 @@ +import { IRagNode } from '@/interfaces/database/flow'; +import { NodeProps, Position } from '@xyflow/react'; +import { memo } from 'react'; +import { NodeHandleId } from '../../constant'; +import { needsSingleStepDebugging } from '../../utils'; +import { CommonHandle } from './handle'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; + +function InnerRagNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + return ( + + + + + + + + ); +} + +export const RagNode = memo(InnerRagNode); diff --git a/web/src/pages/data-flow/canvas/node/invoke-node.tsx b/web/src/pages/data-flow/canvas/node/invoke-node.tsx new file mode 100644 index 000000000..cf1e28d02 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/invoke-node.tsx @@ -0,0 +1,62 @@ +import { useTheme } from '@/components/theme-provider'; +import { IInvokeNode } from '@/interfaces/database/flow'; +import { Handle, NodeProps, Position } from '@xyflow/react'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; + +function InnerInvokeNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const { t } = useTranslation(); + const { theme } = useTheme(); + const url = get(data, 'form.url'); + return ( +
    + + + + +
    {t('flow.url')}
    +
    {url}
    +
    +
    + ); +} + +export const InvokeNode = memo(InnerInvokeNode); diff --git a/web/src/pages/data-flow/canvas/node/iteration-node.tsx b/web/src/pages/data-flow/canvas/node/iteration-node.tsx new file mode 100644 index 000000000..3bdbae590 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/iteration-node.tsx @@ -0,0 +1,93 @@ +import { + IIterationNode, + IIterationStartNode, +} from '@/interfaces/database/flow'; +import { cn } from '@/lib/utils'; +import { NodeProps, NodeResizeControl, Position } from '@xyflow/react'; +import { memo } from 'react'; +import { NodeHandleId, Operator } from '../../constant'; +import OperatorIcon from '../../operator-icon'; +import { CommonHandle } from './handle'; +import { RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ResizeIcon, controlStyle } from './resize-icon'; +import { ToolBar } from './toolbar'; + +export function InnerIterationNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + return ( + +
    + + + + + + + +
    +
    + ); +} + +function InnerIterationStartNode({ + isConnectable = true, + id, + selected, +}: NodeProps) { + return ( + + +
    + +
    +
    + ); +} + +export const IterationStartNode = memo(InnerIterationStartNode); + +export const IterationNode = memo(InnerIterationNode); diff --git a/web/src/pages/data-flow/canvas/node/keyword-node.tsx b/web/src/pages/data-flow/canvas/node/keyword-node.tsx new file mode 100644 index 000000000..012dcf26c --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/keyword-node.tsx @@ -0,0 +1,60 @@ +import LLMLabel from '@/components/llm-select/llm-label'; +import { useTheme } from '@/components/theme-provider'; +import { IKeywordNode } from '@/interfaces/database/flow'; +import { Handle, NodeProps, Position } from '@xyflow/react'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { memo } from 'react'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; + +export function InnerKeywordNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const { theme } = useTheme(); + return ( +
    + + + + + +
    + +
    +
    + ); +} + +export const KeywordNode = memo(InnerKeywordNode); diff --git a/web/src/pages/data-flow/canvas/node/logic-node.tsx b/web/src/pages/data-flow/canvas/node/logic-node.tsx new file mode 100644 index 000000000..481c26c25 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/logic-node.tsx @@ -0,0 +1,41 @@ +import { ILogicNode } from '@/interfaces/database/flow'; +import { NodeProps, Position } from '@xyflow/react'; +import { memo } from 'react'; +import { CommonHandle } from './handle'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; + +export function InnerLogicNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + return ( + + + + + + + + ); +} + +export const LogicNode = memo(InnerLogicNode); diff --git a/web/src/pages/data-flow/canvas/node/message-node.tsx b/web/src/pages/data-flow/canvas/node/message-node.tsx new file mode 100644 index 000000000..057845a63 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/message-node.tsx @@ -0,0 +1,65 @@ +import { IMessageNode } from '@/interfaces/database/flow'; +import { NodeProps, Position } from '@xyflow/react'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { memo } from 'react'; +import { NodeHandleId } from '../../constant'; +import { CommonHandle } from './handle'; +import { LeftHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; + +function InnerMessageNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const messages: string[] = get(data, 'form.messages', []); + return ( + + + + {/* */} + 0, + })} + > + + + {messages.map((message, idx) => { + return ( +
    + {message} +
    + ); + })} +
    +
    +
    + ); +} + +export const MessageNode = memo(InnerMessageNode); diff --git a/web/src/pages/data-flow/canvas/node/node-header.tsx b/web/src/pages/data-flow/canvas/node/node-header.tsx new file mode 100644 index 000000000..9647af1ed --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/node-header.tsx @@ -0,0 +1,34 @@ +import { cn } from '@/lib/utils'; +import { memo } from 'react'; +import { Operator } from '../../constant'; +import OperatorIcon from '../../operator-icon'; +interface IProps { + id: string; + label: string; + name: string; + gap?: number; + className?: string; + wrapperClassName?: string; +} + +const InnerNodeHeader = ({ + label, + name, + className, + wrapperClassName, +}: IProps) => { + return ( +
    +
    + + + {name} + +
    +
    + ); +}; + +const NodeHeader = memo(InnerNodeHeader); + +export default NodeHeader; diff --git a/web/src/pages/data-flow/canvas/node/node-wrapper.tsx b/web/src/pages/data-flow/canvas/node/node-wrapper.tsx new file mode 100644 index 000000000..ab53466ff --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/node-wrapper.tsx @@ -0,0 +1,18 @@ +import { cn } from '@/lib/utils'; +import { HTMLAttributes } from 'react'; + +type IProps = HTMLAttributes & { selected?: boolean }; + +export function NodeWrapper({ children, className, selected }: IProps) { + return ( +
    + {children} +
    + ); +} diff --git a/web/src/pages/data-flow/canvas/node/note-node/index.tsx b/web/src/pages/data-flow/canvas/node/note-node/index.tsx new file mode 100644 index 000000000..2c9d2446e --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/note-node/index.tsx @@ -0,0 +1,104 @@ +import { NodeProps, NodeResizeControl } from '@xyflow/react'; + +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { INoteNode } from '@/interfaces/database/flow'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { NotebookPen } from 'lucide-react'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { NodeWrapper } from '../node-wrapper'; +import { ResizeIcon, controlStyle } from '../resize-icon'; +import { useWatchFormChange, useWatchNameFormChange } from './use-watch-change'; + +const FormSchema = z.object({ + text: z.string(), +}); + +const NameFormSchema = z.object({ + name: z.string(), +}); + +function NoteNode({ data, id, selected }: NodeProps) { + const { t } = useTranslation(); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: data.form, + }); + + const nameForm = useForm>({ + resolver: zodResolver(NameFormSchema), + defaultValues: { name: data.name }, + }); + + useWatchFormChange(id, form); + + useWatchNameFormChange(id, nameForm); + + return ( + + + + +
    + +
    + + ( + + + + + + + )} + /> + + +
    +
    + + ( + + + + + + + )} + /> + ), + [BeginQueryType.Options]: ( + ( + + {props.label} + + ({ + label: x, + value: x as string, + })) ?? [] + } + {...field} + > + + + + )} + /> + ), + [BeginQueryType.File]: ( + + ( +
    + + {t('assistantAvatar')} + + + + + +
    + )} + /> +
    + ), + [BeginQueryType.Integer]: ( + ( + + {props.label} + + + + + + )} + /> + ), + [BeginQueryType.Boolean]: ( + ( + + {props.label} + + + + + + )} + /> + ), + }; + + return ( + BeginQueryTypeMap[q.type as BeginQueryType] ?? + BeginQueryTypeMap[BeginQueryType.Paragraph] + ); + }, + [form, t], + ); + + const onSubmit = useCallback( + (values: z.infer) => { + const nextValues = Object.entries(values).map(([key, value]) => { + const item = parameters[Number(key)]; + return { ...item, value }; + }); + + ok(nextValues); + }, + [formSchemaValues, ok, parameters], + ); + return ( + <> +
    + {message?.data?.tips &&
    {message.data.tips}
    } + + + {parameters.map((x, idx) => { + return
    {renderWidget(x, idx.toString())}
    ; + })} +
    + + {btnText || t(isNext ? 'common.next' : 'flow.run')} + +
    + + +
    + + ); +}; + +export default DebugContent; diff --git a/web/src/pages/data-flow/debug-content/popover-form.tsx b/web/src/pages/data-flow/debug-content/popover-form.tsx new file mode 100644 index 000000000..9465d903b --- /dev/null +++ b/web/src/pages/data-flow/debug-content/popover-form.tsx @@ -0,0 +1,103 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Popover, PopoverContent } from '@/components/ui/popover'; +import { useParseDocument } from '@/hooks/document-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { PropsWithChildren } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +const reg = + /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/; + +const FormSchema = z.object({ + url: z.string(), + result: z.any(), +}); + +const values = { + url: '', + result: null, +}; + +export const PopoverForm = ({ + children, + visible, + switchVisible, +}: PropsWithChildren>) => { + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + const { parseDocument, loading } = useParseDocument(); + const { t } = useTranslation(); + + // useResetFormOnCloseModal({ + // form, + // visible, + // }); + + async function onSubmit(values: z.infer) { + const val = values.url; + + if (reg.test(val)) { + const ret = await parseDocument(val); + if (ret?.data?.code === 0) { + form.setValue('result', ret?.data?.data); + } + } + } + + const content = ( +
    + + ( + + + e.preventDefault()} + placeholder={t('flow.pasteFileLink')} + // suffix={ + // + // } + /> + + + + )} + /> + <>} + /> + + + ); + + return ( + + {children} + {content} + + ); +}; diff --git a/web/src/pages/data-flow/debug-content/uploader.tsx b/web/src/pages/data-flow/debug-content/uploader.tsx new file mode 100644 index 000000000..e11a6f41d --- /dev/null +++ b/web/src/pages/data-flow/debug-content/uploader.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { + FileUpload, + FileUploadDropzone, + FileUploadItem, + FileUploadItemDelete, + FileUploadItemMetadata, + FileUploadItemPreview, + FileUploadItemProgress, + FileUploadList, + FileUploadTrigger, + type FileUploadProps, +} from '@/components/file-upload'; +import { Button } from '@/components/ui/button'; +import { useUploadCanvasFile } from '@/hooks/use-agent-request'; +import { Upload, X } from 'lucide-react'; +import * as React from 'react'; +import { toast } from 'sonner'; + +type FileUploadDirectUploadProps = { + value: Record; + onChange(value: Record): void; +}; + +export function FileUploadDirectUpload({ + onChange, +}: FileUploadDirectUploadProps) { + const [files, setFiles] = React.useState([]); + + const { uploadCanvasFile } = useUploadCanvasFile(); + + const onUpload: NonNullable = React.useCallback( + async (files, { onSuccess, onError }) => { + try { + const uploadPromises = files.map(async (file) => { + const handleError = (error?: any) => { + onError( + file, + error instanceof Error ? error : new Error('Upload failed'), + ); + }; + try { + const ret = await uploadCanvasFile([file]); + if (ret.code === 0) { + onSuccess(file); + onChange(ret.data); + } else { + handleError(); + } + } catch (error) { + handleError(error); + } + }); + + // Wait for all uploads to complete + await Promise.all(uploadPromises); + } catch (error) { + // This handles any error that might occur outside the individual upload processes + console.error('Unexpected error during upload:', error); + } + }, + [onChange, uploadCanvasFile], + ); + + const onFileReject = React.useCallback((file: File, message: string) => { + toast(message, { + description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`, + }); + }, []); + + return ( + + +
    +
    + +
    +

    Drag & drop files here

    +

    + Or click to browse (max 2 files) +

    +
    + + + +
    + + {files.map((file, index) => ( + +
    + + + + + +
    + +
    + ))} +
    +
    + ); +} diff --git a/web/src/pages/data-flow/flow-tooltip.tsx b/web/src/pages/data-flow/flow-tooltip.tsx new file mode 100644 index 000000000..9386dd06b --- /dev/null +++ b/web/src/pages/data-flow/flow-tooltip.tsx @@ -0,0 +1,19 @@ +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const RunTooltip = ({ children }: PropsWithChildren) => { + const { t } = useTranslation(); + return ( + + {children} + +

    {t('flow.testRun')}

    +
    +
    + ); +}; diff --git a/web/src/pages/data-flow/form-hooks.ts b/web/src/pages/data-flow/form-hooks.ts new file mode 100644 index 000000000..fef7d37b9 --- /dev/null +++ b/web/src/pages/data-flow/form-hooks.ts @@ -0,0 +1,43 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { useCallback, useMemo } from 'react'; +import { Operator, RestrictedUpstreamMap } from './constant'; +import useGraphStore from './store'; + +export const useBuildFormSelectOptions = ( + operatorName: Operator, + selfId?: string, // exclude the current node +) => { + const nodes = useGraphStore((state) => state.nodes); + + const buildCategorizeToOptions = useCallback( + (toList: string[]) => { + const excludedNodes: Operator[] = [ + Operator.Note, + ...(RestrictedUpstreamMap[operatorName] ?? []), + ]; + return nodes + .filter( + (x) => + excludedNodes.every((y) => y !== x.data.label) && + x.id !== selfId && + !toList.some((y) => y === x.id), // filter out selected values ​​in other to fields from the current drop-down box options + ) + .map((x) => ({ label: x.data.name, value: x.id })); + }, + [nodes, operatorName, selfId], + ); + + return buildCategorizeToOptions; +}; + +export const useBuildSortOptions = () => { + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return ['data', 'relevance'].map((x) => ({ + value: x, + label: t(x), + })); + }, [t]); + return options; +}; diff --git a/web/src/pages/data-flow/form-sheet/form-config-map.tsx b/web/src/pages/data-flow/form-sheet/form-config-map.tsx new file mode 100644 index 000000000..0fc1b9e2a --- /dev/null +++ b/web/src/pages/data-flow/form-sheet/form-config-map.tsx @@ -0,0 +1,165 @@ +import { Operator } from '../constant'; +import AgentForm from '../form/agent-form'; +import AkShareForm from '../form/akshare-form'; +import ArXivForm from '../form/arxiv-form'; +import BaiduFanyiForm from '../form/baidu-fanyi-form'; +import BaiduForm from '../form/baidu-form'; +import BeginForm from '../form/begin-form'; +import BingForm from '../form/bing-form'; +import CategorizeForm from '../form/categorize-form'; +import CodeForm from '../form/code-form'; +import CrawlerForm from '../form/crawler-form'; +import DeepLForm from '../form/deepl-form'; +import DuckDuckGoForm from '../form/duckduckgo-form'; +import EmailForm from '../form/email-form'; +import ExeSQLForm from '../form/exesql-form'; +import GithubForm from '../form/github-form'; +import GoogleForm from '../form/google-form'; +import GoogleScholarForm from '../form/google-scholar-form'; +import InvokeForm from '../form/invoke-form'; +import IterationForm from '../form/iteration-form'; +import IterationStartForm from '../form/iteration-start-from'; +import Jin10Form from '../form/jin10-form'; +import KeywordExtractForm from '../form/keyword-extract-form'; +import MessageForm from '../form/message-form'; +import PubMedForm from '../form/pubmed-form'; +import QWeatherForm from '../form/qweather-form'; +import RelevantForm from '../form/relevant-form'; +import RetrievalForm from '../form/retrieval-form/next'; +import RewriteQuestionForm from '../form/rewrite-question-form'; +import SearXNGForm from '../form/searxng-form'; +import StringTransformForm from '../form/string-transform-form'; +import SwitchForm from '../form/switch-form'; +import TavilyExtractForm from '../form/tavily-extract-form'; +import TavilyForm from '../form/tavily-form'; +import TuShareForm from '../form/tushare-form'; +import UserFillUpForm from '../form/user-fill-up-form'; +import WenCaiForm from '../form/wencai-form'; +import WikipediaForm from '../form/wikipedia-form'; +import YahooFinanceForm from '../form/yahoo-finance-form'; + +export const FormConfigMap = { + [Operator.Begin]: { + component: BeginForm, + }, + [Operator.Retrieval]: { + component: RetrievalForm, + }, + [Operator.Categorize]: { + component: CategorizeForm, + }, + [Operator.Message]: { + component: MessageForm, + }, + [Operator.Relevant]: { + component: RelevantForm, + }, + [Operator.RewriteQuestion]: { + component: RewriteQuestionForm, + }, + [Operator.Code]: { + component: CodeForm, + }, + [Operator.WaitingDialogue]: { + component: CodeForm, + }, + [Operator.Agent]: { + component: AgentForm, + }, + [Operator.Baidu]: { + component: BaiduForm, + }, + [Operator.DuckDuckGo]: { + component: DuckDuckGoForm, + }, + [Operator.KeywordExtract]: { + component: KeywordExtractForm, + }, + [Operator.Wikipedia]: { + component: WikipediaForm, + }, + [Operator.PubMed]: { + component: PubMedForm, + }, + [Operator.ArXiv]: { + component: ArXivForm, + }, + [Operator.Google]: { + component: GoogleForm, + }, + [Operator.Bing]: { + component: BingForm, + }, + [Operator.GoogleScholar]: { + component: GoogleScholarForm, + }, + [Operator.DeepL]: { + component: DeepLForm, + }, + [Operator.GitHub]: { + component: GithubForm, + }, + [Operator.BaiduFanyi]: { + component: BaiduFanyiForm, + }, + [Operator.QWeather]: { + component: QWeatherForm, + }, + [Operator.ExeSQL]: { + component: ExeSQLForm, + }, + [Operator.Switch]: { + component: SwitchForm, + }, + [Operator.WenCai]: { + component: WenCaiForm, + }, + [Operator.AkShare]: { + component: AkShareForm, + }, + [Operator.YahooFinance]: { + component: YahooFinanceForm, + }, + [Operator.Jin10]: { + component: Jin10Form, + }, + [Operator.TuShare]: { + component: TuShareForm, + }, + [Operator.Crawler]: { + component: CrawlerForm, + }, + [Operator.Invoke]: { + component: InvokeForm, + }, + [Operator.SearXNG]: { + component: SearXNGForm, + }, + [Operator.Concentrator]: { + component: () => <>, + }, + [Operator.Note]: { + component: () => <>, + }, + [Operator.Email]: { + component: EmailForm, + }, + [Operator.Iteration]: { + component: IterationForm, + }, + [Operator.IterationStart]: { + component: IterationStartForm, + }, + [Operator.TavilySearch]: { + component: TavilyForm, + }, + [Operator.UserFillUp]: { + component: UserFillUpForm, + }, + [Operator.StringTransform]: { + component: StringTransformForm, + }, + [Operator.TavilyExtract]: { + component: TavilyExtractForm, + }, +}; diff --git a/web/src/pages/data-flow/form-sheet/next.tsx b/web/src/pages/data-flow/form-sheet/next.tsx new file mode 100644 index 000000000..2d7b5ca8b --- /dev/null +++ b/web/src/pages/data-flow/form-sheet/next.tsx @@ -0,0 +1,134 @@ +import { Input } from '@/components/ui/input'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { useTranslate } from '@/hooks/common-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { cn } from '@/lib/utils'; +import { lowerFirst } from 'lodash'; +import { Play, X } from 'lucide-react'; +import { useMemo } from 'react'; +import { BeginId, Operator } from '../constant'; +import { AgentFormContext } from '../context'; +import { RunTooltip } from '../flow-tooltip'; +import { useHandleNodeNameChange } from '../hooks/use-change-node-name'; +import OperatorIcon from '../operator-icon'; +import useGraphStore from '../store'; +import { needsSingleStepDebugging } from '../utils'; +import { FormConfigMap } from './form-config-map'; +import SingleDebugSheet from './single-debug-sheet'; + +interface IProps { + node?: RAGFlowNodeType; + singleDebugDrawerVisible: IModalProps['visible']; + hideSingleDebugDrawer: IModalProps['hideModal']; + showSingleDebugDrawer: IModalProps['showModal']; + chatVisible: boolean; +} + +const EmptyContent = () =>
    ; + +const FormSheet = ({ + visible, + hideModal, + node, + singleDebugDrawerVisible, + chatVisible, + hideSingleDebugDrawer, + showSingleDebugDrawer, +}: IModalProps & IProps) => { + const operatorName: Operator = node?.data.label as Operator; + const clickedToolId = useGraphStore((state) => state.clickedToolId); + + const currentFormMap = FormConfigMap[operatorName]; + + const OperatorForm = currentFormMap?.component ?? EmptyContent; + + const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({ + id: node?.id, + data: node?.data, + }); + + const isMcp = useMemo(() => { + return ( + operatorName === Operator.Tool && + Object.values(Operator).every((x) => x !== clickedToolId) + ); + }, [clickedToolId, operatorName]); + + const { t } = useTranslate('flow'); + + return ( + + + + +
    +
    + + + {isMcp ? ( +
    MCP Config
    + ) : ( +
    + + {node?.id === BeginId ? ( + {t(BeginId)} + ) : ( + + )} +
    + )} + + {needsSingleStepDebugging(operatorName) && ( + + + + )} + +
    + {isMcp || ( + + {t( + `${lowerFirst(operatorName === Operator.Tool ? clickedToolId : operatorName)}Description`, + )} + + )} +
    +
    +
    + {visible && ( + + + + )} +
    +
    + {singleDebugDrawerVisible && ( + + )} +
    + ); +}; + +export default FormSheet; diff --git a/web/src/pages/data-flow/form-sheet/single-debug-sheet/index.tsx b/web/src/pages/data-flow/form-sheet/single-debug-sheet/index.tsx new file mode 100644 index 000000000..c5fe6e876 --- /dev/null +++ b/web/src/pages/data-flow/form-sheet/single-debug-sheet/index.tsx @@ -0,0 +1,89 @@ +import CopyToClipboard from '@/components/copy-to-clipboard'; +import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet'; +import { useDebugSingle, useFetchInputForm } from '@/hooks/use-agent-request'; +import { IModalProps } from '@/interfaces/common'; +import { cn } from '@/lib/utils'; +import { isEmpty } from 'lodash'; +import { X } from 'lucide-react'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import JsonView from 'react18-json-view'; +import 'react18-json-view/src/style.css'; +import DebugContent from '../../debug-content'; +import { transferInputsArrayToObject } from '../../form/begin-form/use-watch-change'; +import { buildBeginInputListFromObject } from '../../form/begin-form/utils'; + +interface IProps { + componentId?: string; +} + +const SingleDebugSheet = ({ + componentId, + visible, + hideModal, +}: IModalProps & IProps) => { + const { t } = useTranslation(); + const inputForm = useFetchInputForm(componentId); + const { debugSingle, data, loading } = useDebugSingle(); + + const list = useMemo(() => { + return buildBeginInputListFromObject(inputForm); + }, [inputForm]); + + const onOk = useCallback( + (nextValues: any[]) => { + if (componentId) { + debugSingle({ + component_id: componentId, + params: transferInputsArrayToObject(nextValues), + }); + } + }, + [componentId, debugSingle], + ); + + const content = JSON.stringify(data, null, 2); + + return ( + + + +
    + {t('flow.testRun')} + +
    +
    +
    + + {!isEmpty(data) ? ( +
    +
    + JSON + +
    + +
    + ) : null} +
    +
    +
    + ); +}; + +export default SingleDebugSheet; diff --git a/web/src/pages/data-flow/form/agent-form/agent-tools.tsx b/web/src/pages/data-flow/form/agent-form/agent-tools.tsx new file mode 100644 index 000000000..9e5620823 --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/agent-tools.tsx @@ -0,0 +1,191 @@ +import { BlockButton } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { Position } from '@xyflow/react'; +import { t } from 'i18next'; +import { PencilLine, X } from 'lucide-react'; +import { + MouseEventHandler, + PropsWithChildren, + useCallback, + useContext, + useMemo, +} from 'react'; +import { Operator } from '../../constant'; +import { AgentInstanceContext } from '../../context'; +import { useFindMcpById } from '../../hooks/use-find-mcp-by-id'; +import { INextOperatorForm } from '../../interface'; +import OperatorIcon from '../../operator-icon'; +import useGraphStore from '../../store'; +import { filterDownstreamAgentNodeIds } from '../../utils/filter-downstream-nodes'; +import { ToolPopover } from './tool-popover'; +import { useDeleteAgentNodeMCP } from './tool-popover/use-update-mcp'; +import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools'; +import { useGetAgentMCPIds, useGetAgentToolNames } from './use-get-tools'; + +export function ToolCard({ + children, + className, + ...props +}: PropsWithChildren & React.HTMLAttributes) { + const element = useMemo(() => { + return ( +
  • + {children} +
  • + ); + }, [children, className, props]); + + if (children === Operator.Code) { + return ( + + {element} + +

    It doesn't have any config.

    +
    +
    + ); + } + + return element; +} + +type ActionButtonProps = { + record: T; + deleteRecord(record: T): void; + edit: MouseEventHandler; +}; + +function ActionButton({ deleteRecord, record, edit }: ActionButtonProps) { + const handleDelete = useCallback(() => { + deleteRecord(record); + }, [deleteRecord, record]); + + return ( +
    + + +
    + ); +} + +export function AgentTools() { + const { toolNames } = useGetAgentToolNames(); + const { deleteNodeTool } = useDeleteAgentNodeTools(); + const { mcpIds } = useGetAgentMCPIds(); + const { findMcpById } = useFindMcpById(); + const { deleteNodeMCP } = useDeleteAgentNodeMCP(); + const { showFormDrawer } = useContext(AgentInstanceContext); + const { clickedNodeId, findAgentToolNodeById, selectNodeIds } = useGraphStore( + (state) => state, + ); + + const handleEdit: MouseEventHandler = useCallback( + (e) => { + const toolNodeId = findAgentToolNodeById(clickedNodeId); + if (toolNodeId) { + selectNodeIds([toolNodeId]); + showFormDrawer(e, toolNodeId); + } + }, + [clickedNodeId, findAgentToolNodeById, selectNodeIds, showFormDrawer], + ); + + return ( +
    + {t('flow.tools')} +
      + {toolNames.map((x) => ( + +
      + + {x} +
      + +
      + ))} + {mcpIds.map((id) => ( + + {findMcpById(id)?.name} + + + ))} +
    + + {t('flow.addTools')} + +
    + ); +} + +export function Agents({ node }: INextOperatorForm) { + const { addCanvasNode } = useContext(AgentInstanceContext); + const { deleteAgentDownstreamNodesById, edges, getNode, selectNodeIds } = + useGraphStore((state) => state); + const { showFormDrawer } = useContext(AgentInstanceContext); + + const handleEdit = useCallback( + (nodeId: string): MouseEventHandler => + (e) => { + selectNodeIds([nodeId]); + showFormDrawer(e, nodeId); + }, + [selectNodeIds, showFormDrawer], + ); + + const subBottomAgentNodeIds = useMemo(() => { + return filterDownstreamAgentNodeIds(edges, node?.id); + }, [edges, node?.id]); + + return ( +
    + {t('flow.agent')} +
      + {subBottomAgentNodeIds.map((id) => { + const currentNode = getNode(id); + + return ( + + {currentNode?.data.name} + + + ); + })} +
    + + {t('flow.addAgent')} + +
    + ); +} diff --git a/web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx b/web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx new file mode 100644 index 000000000..1cda9fbd5 --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx @@ -0,0 +1,93 @@ +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { X } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { PromptRole } from '../../constant'; +import { PromptEditor } from '../components/prompt-editor'; + +const options = [ + { label: 'User', value: PromptRole.User }, + { label: 'Assistant', value: PromptRole.Assistant }, +]; + +const DynamicPrompt = () => { + const { t } = useTranslation(); + const form = useFormContext(); + const name = 'prompts'; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( + + {t('flow.msg')} +
    + {fields.map((field, index) => ( +
    +
    + ( + + + + + + + + )} + /> + + ( + + +
    + +
    +
    +
    + )} + /> +
    + +
    + ))} +
    + + append({ content: '', role: PromptRole.User })} + > + Add + +
    + ); +}; + +export default memo(DynamicPrompt); diff --git a/web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx b/web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx new file mode 100644 index 000000000..afda465b6 --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx @@ -0,0 +1,63 @@ +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { X } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { PromptEditor } from '../components/prompt-editor'; + +const DynamicTool = () => { + const form = useFormContext(); + const name = 'tools'; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( + +
    + {fields.map((field, index) => ( +
    +
    + ( + + +
    + +
    +
    +
    + )} + /> +
    + +
    + ))} +
    + + append({ component_name: '' })}> + Add + +
    + ); +}; + +export default memo(DynamicTool); diff --git a/web/src/pages/data-flow/form/agent-form/index.tsx b/web/src/pages/data-flow/form/agent-form/index.tsx new file mode 100644 index 000000000..9ca0fb69c --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/index.tsx @@ -0,0 +1,280 @@ +import { Collapse } from '@/components/collapse'; +import { FormContainer } from '@/components/form-container'; +import { + LargeModelFilterFormSchema, + LargeModelFormField, +} from '@/components/large-model-form-field'; +import { LlmSettingSchema } from '@/components/llm-setting-items/next'; +import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from '@/components/ui/form'; +import { Input, NumberInput } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { LlmModelType } from '@/constants/knowledge'; +import { useFindLlmByUuid } from '@/hooks/use-llm-request'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useEffect, useMemo } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { + AgentExceptionMethod, + NodeHandleId, + VariableType, + initialAgentValues, +} from '../../constant'; +import { INextOperatorForm } from '../../interface'; +import useGraphStore from '../../store'; +import { isBottomSubAgent } from '../../utils'; +import { buildOutputList } from '../../utils/build-output-list'; +import { DescriptionField } from '../components/description-field'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { PromptEditor } from '../components/prompt-editor'; +import { QueryVariable } from '../components/query-variable'; +import { AgentTools, Agents } from './agent-tools'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; + +const FormSchema = z.object({ + sys_prompt: z.string(), + description: z.string().optional(), + user_prompt: z.string().optional(), + prompts: z.string().optional(), + // prompts: z + // .array( + // z.object({ + // role: z.string(), + // content: z.string(), + // }), + // ) + // .optional(), + message_history_window_size: z.coerce.number(), + tools: z + .array( + z.object({ + component_name: z.string(), + }), + ) + .optional(), + ...LlmSettingSchema, + max_retries: z.coerce.number(), + delay_after_error: z.coerce.number().optional(), + visual_files_var: z.string().optional(), + max_rounds: z.coerce.number().optional(), + exception_method: z.string().optional(), + exception_goto: z.array(z.string()).optional(), + exception_default_value: z.string().optional(), + ...LargeModelFilterFormSchema, + cite: z.boolean().optional(), +}); + +const outputList = buildOutputList(initialAgentValues.outputs); + +function AgentForm({ node }: INextOperatorForm) { + const { t } = useTranslation(); + const { edges, deleteEdgesBySourceAndSourceHandle } = useGraphStore( + (state) => state, + ); + + const defaultValues = useValues(node); + + const ExceptionMethodOptions = Object.values(AgentExceptionMethod).map( + (x) => ({ + label: t(`flow.${x}`), + value: x, + }), + ); + + const isSubAgent = useMemo(() => { + return isBottomSubAgent(edges, node?.id); + }, [edges, node?.id]); + + const form = useForm>({ + defaultValues: defaultValues, + resolver: zodResolver(FormSchema), + }); + + const llmId = useWatch({ control: form.control, name: 'llm_id' }); + + const findLlmByUuid = useFindLlmByUuid(); + + const exceptionMethod = useWatch({ + control: form.control, + name: 'exception_method', + }); + + useEffect(() => { + if (exceptionMethod !== AgentExceptionMethod.Goto) { + if (node?.id) { + deleteEdgesBySourceAndSourceHandle( + node?.id, + NodeHandleId.AgentException, + ); + } + } + }, [deleteEdgesBySourceAndSourceHandle, exceptionMethod, node?.id]); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + {isSubAgent && } + + {findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && ( + + )} + + + + ( + + {t('flow.systemPrompt')} + + + + + )} + /> + + {isSubAgent || ( + + {/* */} + ( + + {t('flow.userPrompt')} + +
    + +
    +
    +
    + )} + /> +
    + )} + + + + + + {t('flow.advancedSettings')}}> + + + ( + + + {t('flow.cite')} + + + + + + )} + /> + ( + + {t('flow.maxRetries')} + + + + + )} + /> + ( + + {t('flow.delayEfterError')} + + + + + )} + /> + ( + + {t('flow.maxRounds')} + + + + + )} + /> + ( + + {t('flow.exceptionMethod')} + + + + + )} + /> + {exceptionMethod === AgentExceptionMethod.Comment && ( + ( + + {t('flow.ExceptionDefaultValue')} + + + + + )} + /> + )} + + + +
    +
    + ); +} + +export default memo(AgentForm); diff --git a/web/src/pages/data-flow/form/agent-form/tool-popover/index.tsx b/web/src/pages/data-flow/form/agent-form/tool-popover/index.tsx new file mode 100644 index 000000000..84f1ed46a --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/tool-popover/index.tsx @@ -0,0 +1,89 @@ +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Operator } from '@/pages/agent/constant'; +import { AgentFormContext, AgentInstanceContext } from '@/pages/agent/context'; +import useGraphStore from '@/pages/agent/store'; +import { Position } from '@xyflow/react'; +import { t } from 'i18next'; +import { PropsWithChildren, useCallback, useContext, useEffect } from 'react'; +import { useGetAgentMCPIds, useGetAgentToolNames } from '../use-get-tools'; +import { MCPCommand, ToolCommand } from './tool-command'; +import { useUpdateAgentNodeMCP } from './use-update-mcp'; +import { useUpdateAgentNodeTools } from './use-update-tools'; + +enum ToolType { + Common = 'common', + MCP = 'mcp', +} + +export function ToolPopover({ children }: PropsWithChildren) { + const { addCanvasNode } = useContext(AgentInstanceContext); + const node = useContext(AgentFormContext); + const { updateNodeTools } = useUpdateAgentNodeTools(); + const { toolNames } = useGetAgentToolNames(); + const deleteAgentToolNodeById = useGraphStore( + (state) => state.deleteAgentToolNodeById, + ); + const { mcpIds } = useGetAgentMCPIds(); + const { updateNodeMCP } = useUpdateAgentNodeMCP(); + + const handleChange = useCallback( + (value: string[]) => { + if (Array.isArray(value) && node?.id) { + updateNodeTools(value); + } + }, + [node?.id, updateNodeTools], + ); + + useEffect(() => { + const total = toolNames.length + mcpIds.length; + if (node?.id) { + if (total > 0) { + addCanvasNode(Operator.Tool, { + position: Position.Bottom, + nodeId: node?.id, + })(); + } else { + deleteAgentToolNodeById(node.id); + } + } + }, [ + addCanvasNode, + deleteAgentToolNodeById, + mcpIds.length, + node?.id, + toolNames.length, + ]); + + return ( + + {children} + + + + + {t('flow.builtIn')} + + + MCP + + + + + + + + + + + + ); +} diff --git a/web/src/pages/data-flow/form/agent-form/tool-popover/tool-command.tsx b/web/src/pages/data-flow/form/agent-form/tool-popover/tool-command.tsx new file mode 100644 index 000000000..6118c43ab --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/tool-popover/tool-command.tsx @@ -0,0 +1,178 @@ +import { CheckIcon } from 'lucide-react'; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { useListMcpServer } from '@/hooks/use-mcp-request'; +import { cn } from '@/lib/utils'; +import { Operator } from '@/pages/agent/constant'; +import OperatorIcon from '@/pages/agent/operator-icon'; +import { t } from 'i18next'; +import { lowerFirst } from 'lodash'; +import { PropsWithChildren, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const Menus = [ + { + label: t('flow.search'), + list: [ + Operator.TavilySearch, + Operator.TavilyExtract, + Operator.Google, + // Operator.Bing, + Operator.DuckDuckGo, + Operator.Wikipedia, + Operator.SearXNG, + Operator.YahooFinance, + Operator.PubMed, + Operator.GoogleScholar, + Operator.ArXiv, + Operator.WenCai, + ], + }, + { + label: t('flow.communication'), + list: [Operator.Email], + }, + // { + // label: 'Productivity', + // list: [], + // }, + { + label: t('flow.developer'), + list: [Operator.GitHub, Operator.ExeSQL, Operator.Code, Operator.Retrieval], + }, +]; + +type ToolCommandProps = { + value?: string[]; + onChange?(values: string[]): void; +}; + +type ToolCommandItemProps = { + toggleOption(id: string): void; + id: string; + isSelected: boolean; +} & ToolCommandProps; + +function ToolCommandItem({ + toggleOption, + id, + isSelected, + children, +}: ToolCommandItemProps & PropsWithChildren) { + return ( + toggleOption(id)}> +
    + +
    + {children} +
    + ); +} + +function useHandleSelectChange({ onChange, value }: ToolCommandProps) { + const [currentValue, setCurrentValue] = useState([]); + + const toggleOption = useCallback( + (option: string) => { + const newSelectedValues = currentValue.includes(option) + ? currentValue.filter((value) => value !== option) + : [...currentValue, option]; + setCurrentValue(newSelectedValues); + onChange?.(newSelectedValues); + }, + [currentValue, onChange], + ); + + useEffect(() => { + if (Array.isArray(value)) { + setCurrentValue(value); + } + }, [value]); + + return { + toggleOption, + currentValue, + }; +} + +export function ToolCommand({ value, onChange }: ToolCommandProps) { + const { t } = useTranslation(); + const { toggleOption, currentValue } = useHandleSelectChange({ + onChange, + value, + }); + + return ( + + + + No results found. + {Menus.map((x) => ( + + {x.list.map((y) => { + const isSelected = currentValue.includes(y); + return ( + + <> + + {t(`flow.${lowerFirst(y)}`)} + + + ); + })} + + ))} + + + ); +} + +export function MCPCommand({ onChange, value }: ToolCommandProps) { + const { data } = useListMcpServer(); + const { toggleOption, currentValue } = useHandleSelectChange({ + onChange, + value, + }); + + return ( + + + + No results found. + {data.mcp_servers.map((item) => { + const isSelected = currentValue.includes(item.id); + + return ( + + {item.name} + + ); + })} + + + ); +} diff --git a/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-mcp.ts b/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-mcp.ts new file mode 100644 index 000000000..827b9f9e1 --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-mcp.ts @@ -0,0 +1,74 @@ +import { useListMcpServer } from '@/hooks/use-mcp-request'; +import { IAgentForm } from '@/interfaces/database/agent'; +import { AgentFormContext } from '@/pages/agent/context'; +import useGraphStore from '@/pages/agent/store'; +import { get } from 'lodash'; +import { useCallback, useContext, useMemo } from 'react'; + +export function useGetNodeMCP() { + const node = useContext(AgentFormContext); + + return useMemo(() => { + const mcp: IAgentForm['mcp'] = get(node, 'data.form.mcp'); + return mcp; + }, [node]); +} + +export function useUpdateAgentNodeMCP() { + const { updateNodeForm } = useGraphStore((state) => state); + const node = useContext(AgentFormContext); + const mcpList = useGetNodeMCP(); + const { data } = useListMcpServer(); + const mcpServers = data.mcp_servers; + + const findMcpTools = useCallback( + (mcpId: string) => { + const mcp = mcpServers.find((x) => x.id === mcpId); + return mcp?.variables.tools; + }, + [mcpServers], + ); + + const updateNodeMCP = useCallback( + (value: string[]) => { + if (node?.id) { + const nextValue = value.reduce((pre, cur) => { + const mcp = mcpList.find((x) => x.mcp_id === cur); + const tools = findMcpTools(cur); + if (mcp) { + pre.push(mcp); + } else if (tools) { + pre.push({ + mcp_id: cur, + tools: {}, + }); + } + return pre; + }, []); + + updateNodeForm(node?.id, nextValue, ['mcp']); + } + }, + [node?.id, updateNodeForm, mcpList, findMcpTools], + ); + + return { updateNodeMCP }; +} + +export function useDeleteAgentNodeMCP() { + const { updateNodeForm } = useGraphStore((state) => state); + const mcpList = useGetNodeMCP(); + const node = useContext(AgentFormContext); + + const deleteNodeMCP = useCallback( + (value: string) => () => { + const nextMCP = mcpList.filter((x) => x.mcp_id !== value); + if (node?.id) { + updateNodeForm(node?.id, nextMCP, ['mcp']); + } + }, + [node?.id, mcpList, updateNodeForm], + ); + + return { deleteNodeMCP }; +} diff --git a/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-tools.ts b/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-tools.ts new file mode 100644 index 000000000..db579561a --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-tools.ts @@ -0,0 +1,66 @@ +import { IAgentForm } from '@/interfaces/database/agent'; +import { Operator } from '@/pages/agent/constant'; +import { AgentFormContext } from '@/pages/agent/context'; +import { useAgentToolInitialValues } from '@/pages/agent/hooks/use-agent-tool-initial-values'; +import useGraphStore from '@/pages/agent/store'; +import { get } from 'lodash'; +import { useCallback, useContext, useMemo } from 'react'; + +export function useGetNodeTools() { + const node = useContext(AgentFormContext); + + return useMemo(() => { + const tools: IAgentForm['tools'] = get(node, 'data.form.tools'); + return tools; + }, [node]); +} + +export function useUpdateAgentNodeTools() { + const { updateNodeForm } = useGraphStore((state) => state); + const node = useContext(AgentFormContext); + const tools = useGetNodeTools(); + const { initializeAgentToolValues } = useAgentToolInitialValues(); + + const updateNodeTools = useCallback( + (value: string[]) => { + if (node?.id) { + const nextValue = value.reduce((pre, cur) => { + const tool = tools.find((x) => x.component_name === cur); + pre.push( + tool + ? tool + : { + component_name: cur, + name: cur, + params: initializeAgentToolValues(cur as Operator), + }, + ); + return pre; + }, []); + + updateNodeForm(node?.id, nextValue, ['tools']); + } + }, + [initializeAgentToolValues, node?.id, tools, updateNodeForm], + ); + + return { updateNodeTools }; +} + +export function useDeleteAgentNodeTools() { + const { updateNodeForm } = useGraphStore((state) => state); + const tools = useGetNodeTools(); + const node = useContext(AgentFormContext); + + const deleteNodeTool = useCallback( + (value: string) => () => { + const nextTools = tools.filter((x) => x.component_name !== value); + if (node?.id) { + updateNodeForm(node?.id, nextTools, ['tools']); + } + }, + [node?.id, tools, updateNodeForm], + ); + + return { deleteNodeTool }; +} diff --git a/web/src/pages/data-flow/form/agent-form/use-get-tools.ts b/web/src/pages/data-flow/form/agent-form/use-get-tools.ts new file mode 100644 index 000000000..32bf3f0ef --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/use-get-tools.ts @@ -0,0 +1,26 @@ +import { IAgentForm } from '@/interfaces/database/agent'; +import { get } from 'lodash'; +import { useContext, useMemo } from 'react'; +import { AgentFormContext } from '../../context'; + +export function useGetAgentToolNames() { + const node = useContext(AgentFormContext); + + const toolNames = useMemo(() => { + const tools: IAgentForm['tools'] = get(node, 'data.form.tools', []); + return tools.map((x) => x.component_name); + }, [node]); + + return { toolNames }; +} + +export function useGetAgentMCPIds() { + const node = useContext(AgentFormContext); + + const mcpIds = useMemo(() => { + const ids: IAgentForm['mcp'] = get(node, 'data.form.mcp', []); + return ids.map((x) => x.mcp_id); + }, [node]); + + return { mcpIds }; +} diff --git a/web/src/pages/data-flow/form/agent-form/use-values.ts b/web/src/pages/data-flow/form/agent-form/use-values.ts new file mode 100644 index 000000000..b2d61dc9f --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/use-values.ts @@ -0,0 +1,33 @@ +import { useFetchModelId } from '@/hooks/logic-hooks'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { get, isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { initialAgentValues } from '../../constant'; + +export function useValues(node?: RAGFlowNodeType) { + const llmId = useFetchModelId(); + + const defaultValues = useMemo( + () => ({ + ...initialAgentValues, + llm_id: llmId, + prompts: '', + }), + [llmId], + ); + + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + return { + ...formData, + prompts: get(formData, 'prompts.0.content', ''), + }; + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/data-flow/form/agent-form/use-watch-change.ts b/web/src/pages/data-flow/form/agent-form/use-watch-change.ts new file mode 100644 index 000000000..98b0ecf31 --- /dev/null +++ b/web/src/pages/data-flow/form/agent-form/use-watch-change.ts @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { PromptRole } from '../../constant'; +import useGraphStore from '../../store'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = { + ...values, + prompts: [{ role: PromptRole.User, content: values.prompts }], + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/form/akshare-form/index.tsx b/web/src/pages/data-flow/form/akshare-form/index.tsx new file mode 100644 index 000000000..1cfd554b1 --- /dev/null +++ b/web/src/pages/data-flow/form/akshare-form/index.tsx @@ -0,0 +1,22 @@ +import { TopNFormField } from '@/components/top-n-item'; +import { Form } from '@/components/ui/form'; +import { INextOperatorForm } from '../../interface'; +import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; + +const AkShareForm = ({ form, node }: INextOperatorForm) => { + return ( +
    + { + e.preventDefault(); + }} + > + + +
    + + ); +}; + +export default AkShareForm; diff --git a/web/src/pages/data-flow/form/arxiv-form/index.tsx b/web/src/pages/data-flow/form/arxiv-form/index.tsx new file mode 100644 index 000000000..a6e1b7c45 --- /dev/null +++ b/web/src/pages/data-flow/form/arxiv-form/index.tsx @@ -0,0 +1,96 @@ +import { FormContainer } from '@/components/form-container'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialArXivValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const ArXivFormPartialSchema = { + top_n: z.number(), + sort_by: z.string(), +}; + +export const FormSchema = z.object({ + ...ArXivFormPartialSchema, + query: z.string(), +}); + +export function ArXivFormWidgets() { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return ['submittedDate', 'lastUpdatedDate', 'relevance'].map((x) => ({ + value: x, + label: t(x), + })); + }, [t]); + + return ( + <> + + ( + + {t('sortBy')} + + + + + + )} + /> + + ); +} + +const outputList = buildOutputList(initialArXivValues.outputs); + +function ArXivForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialArXivValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + +
    + +
    +
    + ); +} + +export default memo(ArXivForm); diff --git a/web/src/pages/data-flow/form/baidu-fanyi-form/index.tsx b/web/src/pages/data-flow/form/baidu-fanyi-form/index.tsx new file mode 100644 index 000000000..c02b3dd85 --- /dev/null +++ b/web/src/pages/data-flow/form/baidu-fanyi-form/index.tsx @@ -0,0 +1,71 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { IOperatorForm } from '../../interface'; +import { + BaiduFanyiDomainOptions, + BaiduFanyiSourceLangOptions, +} from '../../options'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const options = useMemo(() => { + return ['translate', 'fieldtranslate'].map((x) => ({ + value: x, + label: t(`baiduSecretKeyOptions.${x}`), + })); + }, [t]); + + const baiduFanyiOptions = useMemo(() => { + return BaiduFanyiDomainOptions.map((x) => ({ + value: x, + label: t(`baiduDomainOptions.${x}`), + })); + }, [t]); + + const baiduFanyiSourceLangOptions = useMemo(() => { + return BaiduFanyiSourceLangOptions.map((x) => ({ + value: x, + label: t(`baiduSourceLangOptions.${x}`), + })); + }, [t]); + + return ( +
    + + + + + + + + + + + + {({ getFieldValue }) => + getFieldValue('trans_type') === 'fieldtranslate' && ( + + + + ) + } + + + + + + + +
    + ); +}; + +export default BaiduFanyiForm; diff --git a/web/src/pages/data-flow/form/baidu-form/index.tsx b/web/src/pages/data-flow/form/baidu-form/index.tsx new file mode 100644 index 000000000..0861ef829 --- /dev/null +++ b/web/src/pages/data-flow/form/baidu-form/index.tsx @@ -0,0 +1,22 @@ +import { TopNFormField } from '@/components/top-n-item'; +import { Form } from '@/components/ui/form'; +import { INextOperatorForm } from '../../interface'; +import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; + +const BaiduForm = ({ form, node }: INextOperatorForm) => { + return ( +
    + { + e.preventDefault(); + }} + > + + +
    + + ); +}; + +export default BaiduForm; diff --git a/web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx b/web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx new file mode 100644 index 000000000..12b2bfb4b --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { X } from 'lucide-react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +export function BeginDynamicOptions() { + const { t } = useTranslation(); + const form = useFormContext(); + const name = 'options'; + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
    + {fields.map((field, index) => { + const typeField = `${name}.${index}.value`; + return ( +
    + ( + + + + + + + )} + /> + +
    + ); + })} + append({ value: '' })} type="button"> + {t('flow.addField')} + +
    + ); +} diff --git a/web/src/pages/data-flow/form/begin-form/index.tsx b/web/src/pages/data-flow/form/begin-form/index.tsx new file mode 100644 index 000000000..ad4eb9d3e --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/index.tsx @@ -0,0 +1,205 @@ +import { Collapse } from '@/components/collapse'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; +import { FormTooltip } from '@/components/ui/tooltip'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { t } from 'i18next'; +import { Plus } from 'lucide-react'; +import { memo, useEffect, useRef } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { AgentDialogueMode } from '../../constant'; +import { INextOperatorForm } from '../../interface'; +import { ParameterDialog } from './parameter-dialog'; +import { QueryTable } from './query-table'; +import { useEditQueryRecord } from './use-edit-query'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; + +const ModeOptions = [ + { value: AgentDialogueMode.Conversational, label: t('flow.conversational') }, + { value: AgentDialogueMode.Task, label: t('flow.task') }, +]; + +function BeginForm({ node }: INextOperatorForm) { + const { t } = useTranslation(); + + const values = useValues(node); + + const FormSchema = z.object({ + enablePrologue: z.boolean().optional(), + prologue: z.string().trim().optional(), + mode: z.string(), + inputs: z + .array( + z.object({ + key: z.string(), + type: z.string(), + value: z.string(), + optional: z.boolean(), + name: z.string(), + options: z.array(z.union([z.number(), z.string(), z.boolean()])), + }), + ) + .optional(), + }); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + const inputs = useWatch({ control: form.control, name: 'inputs' }); + const mode = useWatch({ control: form.control, name: 'mode' }); + + const enablePrologue = useWatch({ + control: form.control, + name: 'enablePrologue', + }); + + const previousModeRef = useRef(mode); + + useEffect(() => { + if ( + previousModeRef.current === AgentDialogueMode.Task && + mode === AgentDialogueMode.Conversational + ) { + form.setValue('enablePrologue', true); + } + previousModeRef.current = mode; + }, [mode, form]); + + const { + ok, + currentRecord, + visible, + hideModal, + showModal, + otherThanCurrentQuery, + handleDeleteRecord, + } = useEditQueryRecord({ + form, + node, + }); + + return ( +
    +
    + ( + + + {t('flow.mode')} + + + + + + + )} + /> + {mode === AgentDialogueMode.Conversational && ( + ( + + + {t('flow.openingSwitch')} + + + + + + + )} + /> + )} + {mode === AgentDialogueMode.Conversational && enablePrologue && ( + ( + + + {t('flow.openingCopy')} + + + + + + + )} + /> + )} + {/* Create a hidden field to make Form instance record this */} +
    } + /> + + {t('flow.input')} + + + } + rightContent={ + + } + > + + + {visible && ( + + )} + +
    + ); +} + +export default memo(BeginForm); diff --git a/web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx b/web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx new file mode 100644 index 000000000..3b9070437 --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx @@ -0,0 +1,226 @@ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { useTranslate } from '@/hooks/common-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { isEmpty } from 'lodash'; +import { ChangeEvent, useEffect, useMemo } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; +import { BeginQuery } from '../../interface'; +import { BeginDynamicOptions } from './begin-dynamic-options'; + +type ModalFormProps = { + initialValue: BeginQuery; + otherThanCurrentQuery: BeginQuery[]; + submit(values: any): void; +}; + +const FormId = 'BeginParameterForm'; + +function ParameterForm({ + initialValue, + otherThanCurrentQuery, + submit, +}: ModalFormProps) { + const { t } = useTranslate('flow'); + const FormSchema = z.object({ + type: z.string(), + key: z + .string() + .trim() + .min(1) + .refine( + (value) => + !value || !otherThanCurrentQuery.some((x) => x.key === value), + { message: 'The key cannot be repeated!' }, + ), + optional: z.boolean(), + name: z.string().trim().min(1), + options: z + .array(z.object({ value: z.string().or(z.boolean()).or(z.number()) })) + .optional(), + }); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + mode: 'onChange', + defaultValues: { + type: BeginQueryType.Line, + optional: false, + key: '', + name: '', + options: [], + }, + }); + + const options = useMemo(() => { + return Object.values(BeginQueryType).reduce( + (pre, cur) => { + const Icon = BeginQueryTypeIconMap[cur]; + + return [ + ...pre, + { + label: ( +
    + + {t(cur.toLowerCase())} +
    + ), + value: cur, + }, + ]; + }, + [], + ); + }, []); + + const type = useWatch({ + control: form.control, + name: 'type', + }); + + useEffect(() => { + if (!isEmpty(initialValue)) { + form.reset({ + ...initialValue, + options: initialValue.options?.map((x) => ({ value: x })), + }); + } + }, [form, initialValue]); + + function onSubmit(data: z.infer) { + const values = { ...data, options: data.options?.map((x) => x.value) }; + console.log('🚀 ~ onSubmit ~ values:', values); + + submit(values); + } + + const handleKeyChange = (e: ChangeEvent) => { + const name = form.getValues().name || ''; + form.setValue('key', e.target.value.trim()); + if (!name) { + form.setValue('name', e.target.value.trim()); + } + }; + return ( +
    + + ( + + {t('type')} + + + + + + )} + /> + ( + + {t('key')} + + + + + + )} + /> + ( + + {t('name')} + + + + + + )} + /> + ( + + {t('optional')} + + + + + + )} + /> + {type === BeginQueryType.Options && ( + + )} + + + ); +} + +export function ParameterDialog({ + initialValue, + hideModal, + otherThanCurrentQuery, + submit, +}: ModalFormProps & IModalProps) { + const { t } = useTranslation(); + + return ( + + + + {t('flow.variableSettings')} + + + + + + + + ); +} diff --git a/web/src/pages/data-flow/form/begin-form/query-table.tsx b/web/src/pages/data-flow/form/begin-form/query-table.tsx new file mode 100644 index 000000000..5701c49b1 --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/query-table.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { Pencil, Trash2 } from 'lucide-react'; +import * as React from 'react'; + +import { TableEmpty } from '@/components/table-skeleton'; +import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; +import { BeginQuery } from '../../interface'; + +interface IProps { + data: BeginQuery[]; + deleteRecord(index: number): void; + showModal(index: number, record: BeginQuery): void; +} + +export function QueryTable({ data = [], deleteRecord, showModal }: IProps) { + const { t } = useTranslation(); + + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + + const columns: ColumnDef[] = [ + { + accessorKey: 'key', + header: t('flow.key'), + meta: { cellClassName: 'max-w-30' }, + cell: ({ row }) => { + const key: string = row.getValue('key'); + return ( + + +
    {key}
    +
    + +

    {key}

    +
    +
    + ); + }, + }, + { + accessorKey: 'name', + header: t('flow.name'), + meta: { cellClassName: 'max-w-30' }, + cell: ({ row }) => { + const name: string = row.getValue('name'); + return ( + + +
    {name}
    +
    + +

    {name}

    +
    +
    + ); + }, + }, + { + accessorKey: 'type', + header: t('flow.type'), + cell: ({ row }) => ( +
    + {t(`flow.${(row.getValue('type')?.toString() || '').toLowerCase()}`)} +
    + ), + }, + { + accessorKey: 'optional', + header: t('flow.optional'), + cell: ({ row }) =>
    {row.getValue('optional') ? 'Yes' : 'No'}
    , + }, + { + id: 'actions', + enableHiding: false, + header: t('common.action'), + cell: ({ row }) => { + const record = row.original; + const idx = row.index; + + return ( +
    + + +
    + ); + }, + }, + ]; + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnFilters, + columnVisibility, + }, + }); + + return ( +
    +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + )} + +
    +
    +
    + ); +} diff --git a/web/src/pages/data-flow/form/begin-form/use-edit-query.ts b/web/src/pages/data-flow/form/begin-form/use-edit-query.ts new file mode 100644 index 000000000..6942ba88b --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/use-edit-query.ts @@ -0,0 +1,67 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import { useSetSelectedRecord } from '@/hooks/logic-hooks'; +import { useCallback, useMemo, useState } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { BeginQuery, INextOperatorForm } from '../../interface'; + +export const useEditQueryRecord = ({ + form, +}: INextOperatorForm & { form: UseFormReturn }) => { + const { setRecord, currentRecord } = useSetSelectedRecord(); + const { visible, hideModal, showModal } = useSetModalState(); + const [index, setIndex] = useState(-1); + const inputs: BeginQuery[] = useWatch({ + control: form.control, + name: 'inputs', + }); + + const otherThanCurrentQuery = useMemo(() => { + return inputs.filter((item, idx) => idx !== index); + }, [index, inputs]); + + const handleEditRecord = useCallback( + (record: BeginQuery) => { + const inputs: BeginQuery[] = form?.getValues('inputs') || []; + + const nextQuery: BeginQuery[] = + index > -1 ? inputs.toSpliced(index, 1, record) : [...inputs, record]; + + form.setValue('inputs', nextQuery); + + hideModal(); + }, + [form, hideModal, index], + ); + + const handleShowModal = useCallback( + (idx?: number, record?: BeginQuery) => { + setIndex(idx ?? -1); + setRecord(record ?? ({} as BeginQuery)); + showModal(); + }, + [setRecord, showModal], + ); + + const handleDeleteRecord = useCallback( + (idx: number) => { + const inputs = form?.getValues('inputs') || []; + const nextInputs = inputs.filter( + (item: BeginQuery, index: number) => index !== idx, + ); + + form.setValue('inputs', nextInputs); + }, + [form], + ); + + return { + ok: handleEditRecord, + currentRecord, + setRecord, + visible, + hideModal, + showModal: handleShowModal, + otherThanCurrentQuery, + handleDeleteRecord, + }; +}; diff --git a/web/src/pages/data-flow/form/begin-form/use-values.ts b/web/src/pages/data-flow/form/begin-form/use-values.ts new file mode 100644 index 000000000..10326bae8 --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/use-values.ts @@ -0,0 +1,34 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AgentDialogueMode } from '../../constant'; +import { buildBeginInputListFromObject } from './utils'; + +export function useValues(node?: RAGFlowNodeType) { + const { t } = useTranslation(); + + const defaultValues = useMemo( + () => ({ + enablePrologue: true, + prologue: t('chat.setAnOpenerInitial'), + mode: AgentDialogueMode.Conversational, + inputs: [], + }), + [t], + ); + + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + const inputs = buildBeginInputListFromObject(formData?.inputs); + + return { ...(formData || {}), inputs }; + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/data-flow/form/begin-form/use-watch-change.ts b/web/src/pages/data-flow/form/begin-form/use-watch-change.ts new file mode 100644 index 000000000..f0da58068 --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/use-watch-change.ts @@ -0,0 +1,31 @@ +import { omit } from 'lodash'; +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { BeginQuery } from '../../interface'; +import useGraphStore from '../../store'; + +export function transferInputsArrayToObject(inputs: BeginQuery[] = []) { + return inputs.reduce>>((pre, cur) => { + pre[cur.key] = omit(cur, 'key'); + + return pre; + }, {}); +} + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + if (id) { + values = form?.getValues() || {}; + + const nextValues = { + ...values, + inputs: transferInputsArrayToObject(values.inputs), + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/form/begin-form/utils.ts b/web/src/pages/data-flow/form/begin-form/utils.ts new file mode 100644 index 000000000..36038c4f6 --- /dev/null +++ b/web/src/pages/data-flow/form/begin-form/utils.ts @@ -0,0 +1,14 @@ +import { BeginQuery } from '../../interface'; + +export function buildBeginInputListFromObject( + inputs: Record>, +) { + return Object.entries(inputs || {}).reduce( + (pre, [key, value]) => { + pre.push({ ...(value || {}), key }); + + return pre; + }, + [], + ); +} diff --git a/web/src/pages/data-flow/form/bing-form/index.tsx b/web/src/pages/data-flow/form/bing-form/index.tsx new file mode 100644 index 000000000..fae75aa12 --- /dev/null +++ b/web/src/pages/data-flow/form/bing-form/index.tsx @@ -0,0 +1,131 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialBingValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { BingCountryOptions, BingLanguageOptions } from '../../options'; +import { FormWrapper } from '../components/form-wrapper'; +import { QueryVariable } from '../components/query-variable'; + +export const BingFormSchema = { + channel: z.string(), + api_key: z.string(), + country: z.string(), + language: z.string(), + top_n: z.number(), +}; + +export const FormSchema = z.object({ + query: z.string().optional(), + ...BingFormSchema, +}); + +export function BingFormWidgets() { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return ['Webpages', 'News'].map((x) => ({ label: x, value: x })); + }, []); + + return ( + <> + + ( + + {t('channel')} + + + + + + )} + /> + ( + + {t('apiKey')} + + + + + + )} + /> + ( + + {t('country')} + + + + + + )} + /> + ( + + {t('language')} + + + + + + )} + /> + + ); +} + +function BingForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialBingValues, node); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues, + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + +
    + ); +} + +export default memo(BingForm); diff --git a/web/src/pages/data-flow/form/categorize-form/dynamic-categorize.tsx b/web/src/pages/data-flow/form/categorize-form/dynamic-categorize.tsx new file mode 100644 index 000000000..0807f7bfa --- /dev/null +++ b/web/src/pages/data-flow/form/categorize-form/dynamic-categorize.tsx @@ -0,0 +1,249 @@ +import { Button } from '@/components/ui/button'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { BlurTextarea } from '@/components/ui/textarea'; +import { useTranslate } from '@/hooks/common-hooks'; +import { PlusOutlined } from '@ant-design/icons'; +import { useUpdateNodeInternals } from '@xyflow/react'; +import humanId from 'human-id'; +import trim from 'lodash/trim'; +import { ChevronsUpDown, X } from 'lucide-react'; +import { + ChangeEventHandler, + FocusEventHandler, + memo, + useCallback, + useEffect, + useState, +} from 'react'; +import { UseFormReturn, useFieldArray, useFormContext } from 'react-hook-form'; +import { v4 as uuid } from 'uuid'; +import { z } from 'zod'; +import useGraphStore from '../../store'; +import DynamicExample from './dynamic-example'; +import { useCreateCategorizeFormSchema } from './use-form-schema'; + +interface IProps { + nodeId?: string; +} + +interface INameInputProps { + value?: string; + onChange?: (value: string) => void; + otherNames?: string[]; + validate(error?: string): void; +} + +const getOtherFieldValues = ( + form: UseFormReturn, + formListName: string = 'items', + index: number, + latestField: string, +) => + (form.getValues(formListName) ?? []) + .map((x: any) => x[latestField]) + .filter( + (x: string) => + x !== form.getValues(`${formListName}.${index}.${latestField}`), + ); + +const InnerNameInput = ({ + value, + onChange, + otherNames, + validate, +}: INameInputProps) => { + const [name, setName] = useState(); + const { t } = useTranslate('flow'); + + const handleNameChange: ChangeEventHandler = useCallback( + (e) => { + const val = e.target.value; + setName(val); + const trimmedVal = trim(val); + // trigger validation + if (otherNames?.some((x) => x === trimmedVal)) { + validate(t('nameRepeatedMsg')); + } else if (trimmedVal === '') { + validate(t('nameRequiredMsg')); + } else { + validate(''); + } + }, + [otherNames, validate, t], + ); + + const handleNameBlur: FocusEventHandler = useCallback( + (e) => { + const val = e.target.value; + if (otherNames?.every((x) => x !== val) && trim(val) !== '') { + onChange?.(val); + } + }, + [onChange, otherNames], + ); + + useEffect(() => { + setName(value); + }, [value]); + + return ( + + ); +}; + +const NameInput = memo(InnerNameInput); + +const InnerFormSet = ({ index }: IProps & { index: number }) => { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + const buildFieldName = useCallback( + (name: string) => { + return `items.${index}.${name}`; + }, + [index], + ); + + return ( +
    + ( + + {t('categoryName')} + + { + const fieldName = buildFieldName('name'); + if (error) { + form.setError(fieldName, { message: error }); + } else { + form.clearErrors(fieldName); + } + }} + > + + + + )} + /> + ( + + {t('description')} + + + + + + )} + /> + {/* Create a hidden field to make Form instance record this */} +
    } + /> + +
    + ); +}; + +const FormSet = memo(InnerFormSet); + +const DynamicCategorize = ({ nodeId }: IProps) => { + const updateNodeInternals = useUpdateNodeInternals(); + const FormSchema = useCreateCategorizeFormSchema(); + + const deleteCategorizeCaseEdges = useGraphStore( + (state) => state.deleteEdgesBySourceAndSourceHandle, + ); + const form = useFormContext>(); + const { t } = useTranslate('flow'); + const { fields, remove, append } = useFieldArray({ + name: 'items', + control: form.control, + }); + + const handleAdd = useCallback(() => { + append({ + name: humanId(), + description: '', + uuid: uuid(), + examples: [{ value: '' }], + }); + if (nodeId) updateNodeInternals(nodeId); + }, [append, nodeId, updateNodeInternals]); + + const handleRemove = useCallback( + (index: number) => () => { + remove(index); + if (nodeId) { + const uuid = fields[index].uuid; + deleteCategorizeCaseEdges(nodeId, uuid); + } + }, + [deleteCategorizeCaseEdges, fields, nodeId, remove], + ); + + return ( +
    + {fields.map((field, index) => ( + +
    +

    + {form.getValues(`items.${index}.name`)} +

    + +
    + + +
    +
    +
    + + + +
    + ))} + + +
    + ); +}; + +export default memo(DynamicCategorize); diff --git a/web/src/pages/data-flow/form/categorize-form/dynamic-example.tsx b/web/src/pages/data-flow/form/categorize-form/dynamic-example.tsx new file mode 100644 index 000000000..35d95cbc6 --- /dev/null +++ b/web/src/pages/data-flow/form/categorize-form/dynamic-example.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Textarea } from '@/components/ui/textarea'; +import { Plus, X } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +type DynamicExampleProps = { name: string }; + +const DynamicExample = ({ name }: DynamicExampleProps) => { + const { t } = useTranslation(); + const form = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( + + {t('flow.examples')} +
    + {fields.map((field, index) => ( +
    + ( + + + + + + )} + /> + {index === 0 ? ( + + ) : ( + + )} +
    + ))} +
    + +
    + ); +}; + +export default memo(DynamicExample); diff --git a/web/src/pages/data-flow/form/categorize-form/index.tsx b/web/src/pages/data-flow/form/categorize-form/index.tsx new file mode 100644 index 000000000..c36ff4529 --- /dev/null +++ b/web/src/pages/data-flow/form/categorize-form/index.tsx @@ -0,0 +1,48 @@ +import { FormContainer } from '@/components/form-container'; +import { LargeModelFormField } from '@/components/large-model-form-field'; +import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; +import { Form } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { initialCategorizeValues } from '../../constant'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; +import DynamicCategorize from './dynamic-categorize'; +import { useCreateCategorizeFormSchema } from './use-form-schema'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; + +const outputList = buildOutputList(initialCategorizeValues.outputs); + +function CategorizeForm({ node }: INextOperatorForm) { + const values = useValues(node); + + const FormSchema = useCreateCategorizeFormSchema(); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + + +
    + ); +} + +export default memo(CategorizeForm); diff --git a/web/src/pages/data-flow/form/categorize-form/use-form-schema.ts b/web/src/pages/data-flow/form/categorize-form/use-form-schema.ts new file mode 100644 index 000000000..9e56bb18b --- /dev/null +++ b/web/src/pages/data-flow/form/categorize-form/use-form-schema.ts @@ -0,0 +1,32 @@ +import { LlmSettingSchema } from '@/components/llm-setting-items/next'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +export function useCreateCategorizeFormSchema() { + const { t } = useTranslation(); + + const FormSchema = z.object({ + query: z.string().optional(), + parameter: z.string().optional(), + ...LlmSettingSchema, + message_history_window_size: z.coerce.number(), + items: z.array( + z + .object({ + name: z.string().min(1, t('flow.nameMessage')).trim(), + description: z.string().optional(), + uuid: z.string(), + examples: z + .array( + z.object({ + value: z.string(), + }), + ) + .optional(), + }) + .optional(), + ), + }); + + return FormSchema; +} diff --git a/web/src/pages/data-flow/form/categorize-form/use-values.ts b/web/src/pages/data-flow/form/categorize-form/use-values.ts new file mode 100644 index 000000000..a920ec4cc --- /dev/null +++ b/web/src/pages/data-flow/form/categorize-form/use-values.ts @@ -0,0 +1,34 @@ +import { ModelVariableType } from '@/constants/knowledge'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty, isPlainObject } from 'lodash'; +import { useMemo } from 'react'; + +const defaultValues = { + parameter: ModelVariableType.Precise, + message_history_window_size: 1, + temperatureEnabled: true, + topPEnabled: true, + presencePenaltyEnabled: true, + frequencyPenaltyEnabled: true, + maxTokensEnabled: true, + items: [], +}; + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + if (isEmpty(formData)) { + return defaultValues; + } + if (isPlainObject(formData)) { + // const nextValues = { + // ...omit(formData, 'category_description'), + // items, + // }; + + return formData; + } + }, [node]); + + return values; +} diff --git a/web/src/pages/data-flow/form/categorize-form/use-watch-change.ts b/web/src/pages/data-flow/form/categorize-form/use-watch-change.ts new file mode 100644 index 000000000..a97b80a77 --- /dev/null +++ b/web/src/pages/data-flow/form/categorize-form/use-watch-change.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id) { + values = form?.getValues(); + + updateNodeForm(id, { ...values, items: values.items?.slice() || [] }); + } + }, [id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/form/code-form/index.tsx b/web/src/pages/data-flow/form/code-form/index.tsx new file mode 100644 index 000000000..2883fdf46 --- /dev/null +++ b/web/src/pages/data-flow/form/code-form/index.tsx @@ -0,0 +1,168 @@ +import Editor, { loader } from '@monaco-editor/react'; +import { INextOperatorForm } from '../../interface'; + +import { FormContainer } from '@/components/form-container'; +import { useIsDarkTheme } from '@/components/theme-provider'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { ProgrammingLanguage } from '@/constants/agent'; +import { ICodeForm } from '@/interfaces/database/agent'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { + DynamicInputVariable, + TypeOptions, + VariableTitle, +} from './next-variable'; +import { FormSchema, FormSchemaType } from './schema'; +import { useValues } from './use-values'; +import { + useHandleLanguageChange, + useWatchFormChange, +} from './use-watch-change'; + +loader.config({ paths: { vs: '/vs' } }); + +const options = [ + ProgrammingLanguage.Python, + ProgrammingLanguage.Javascript, +].map((x) => ({ value: x, label: x })); + +const DynamicFieldName = 'outputs'; + +function CodeForm({ node }: INextOperatorForm) { + const formData = node?.data.form as ICodeForm; + const { t } = useTranslation(); + const values = useValues(node); + const isDarkTheme = useIsDarkTheme(); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + const handleLanguageChange = useHandleLanguageChange(node?.id, form); + + return ( +
    + + + ( + + + Code + ( + + + { + field.onChange(val); + handleLanguageChange(val); + }} + options={options} + /> + + + + )} + /> + + + + + + + )} + /> + + {formData.lang === ProgrammingLanguage.Python ? ( + + ) : ( +
    + + + ( + + Name + + + + + + )} + /> + ( + + Type + + + + + + )} + /> + +
    + )} +
    +
    + +
    +
    + ); +} + +export default memo(CodeForm); diff --git a/web/src/pages/data-flow/form/code-form/next-variable.tsx b/web/src/pages/data-flow/form/code-form/next-variable.tsx new file mode 100644 index 000000000..39a2dd4a4 --- /dev/null +++ b/web/src/pages/data-flow/form/code-form/next-variable.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { FormContainer } from '@/components/form-container'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { BlurInput } from '@/components/ui/input'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { X } from 'lucide-react'; +import { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; + +interface IProps { + node?: RAGFlowNodeType; + name?: string; + isOutputs: boolean; +} + +export const TypeOptions = [ + 'String', + 'Number', + 'Boolean', + 'Array', + 'Array', + 'Object', +].map((x) => ({ label: x, value: x })); + +export function DynamicVariableForm({ name = 'arguments', isOutputs }: IProps) { + const { t } = useTranslation(); + const form = useFormContext(); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + const nextOptions = useBuildQueryVariableOptions(); + + return ( +
    + {fields.map((field, index) => { + const typeField = `${name}.${index}.name`; + return ( +
    + ( + + + + + + + )} + /> + + ( + + + {isOutputs ? ( + + ) : ( + + )} + + + + )} + /> + +
    + ); + })} + append({ name: '', type: undefined })}> + {t('flow.addVariable')} + +
    + ); +} + +export function VariableTitle({ title }: { title: ReactNode }) { + return
    {title}
    ; +} + +export function DynamicInputVariable({ + node, + name, + title, + isOutputs = false, +}: IProps & { title: ReactNode }) { + return ( +
    + + + + +
    + ); +} diff --git a/web/src/pages/data-flow/form/code-form/schema.ts b/web/src/pages/data-flow/form/code-form/schema.ts new file mode 100644 index 000000000..fe694444e --- /dev/null +++ b/web/src/pages/data-flow/form/code-form/schema.ts @@ -0,0 +1,14 @@ +import { ProgrammingLanguage } from '@/constants/agent'; +import { z } from 'zod'; + +export const FormSchema = z.object({ + lang: z.enum([ProgrammingLanguage.Python, ProgrammingLanguage.Javascript]), + script: z.string(), + arguments: z.array(z.object({ name: z.string(), type: z.string() })), + outputs: z.union([ + z.array(z.object({ name: z.string(), type: z.string() })).optional(), + z.object({ name: z.string(), type: z.string() }), + ]), +}); + +export type FormSchemaType = z.infer; diff --git a/web/src/pages/data-flow/form/code-form/use-values.ts b/web/src/pages/data-flow/form/code-form/use-values.ts new file mode 100644 index 000000000..ea6f2d67c --- /dev/null +++ b/web/src/pages/data-flow/form/code-form/use-values.ts @@ -0,0 +1,47 @@ +import { ProgrammingLanguage } from '@/constants/agent'; +import { ICodeForm } from '@/interfaces/database/agent'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { initialCodeValues } from '../../constant'; + +function convertToArray(args: Record) { + return Object.entries(args).map(([key, value]) => ({ + name: key, + type: value, + })); +} + +type OutputsFormType = { name: string; type: string }; + +function convertOutputsToArray({ lang, outputs = {} }: ICodeForm) { + if (lang === ProgrammingLanguage.Python) { + return Object.entries(outputs).map(([key, val]) => ({ + name: key, + type: val.type, + })); + } + return Object.entries(outputs).reduce((pre, [key, val]) => { + pre.name = key; + pre.type = val.type; + return pre; + }, {} as OutputsFormType); +} + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return initialCodeValues; + } + + return { + ...formData, + arguments: convertToArray(formData.arguments), + outputs: convertOutputsToArray(formData), + }; + }, [node?.data?.form]); + + return values; +} diff --git a/web/src/pages/data-flow/form/code-form/use-watch-change.ts b/web/src/pages/data-flow/form/code-form/use-watch-change.ts new file mode 100644 index 000000000..80e0c8b15 --- /dev/null +++ b/web/src/pages/data-flow/form/code-form/use-watch-change.ts @@ -0,0 +1,95 @@ +import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; +import { ICodeForm } from '@/interfaces/database/agent'; +import { isEmpty } from 'lodash'; +import { useCallback, useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; +import { FormSchemaType } from './schema'; + +function convertToObject(list: FormSchemaType['arguments'] = []) { + return list.reduce>((pre, cur) => { + pre[cur.name] = cur.type; + + return pre; + }, {}); +} + +type ArrayOutputs = Extract>; + +type ObjectOutputs = Exclude>; + +function convertOutputsToObject({ lang, outputs }: FormSchemaType) { + if (lang === ProgrammingLanguage.Python) { + return (outputs as ArrayOutputs).reduce( + (pre, cur) => { + pre[cur.name] = { + value: '', + type: cur.type, + }; + + return pre; + }, + {}, + ); + } + const outputsObject = outputs as ObjectOutputs; + if (isEmpty(outputsObject)) { + return {}; + } + return { + [outputsObject.name]: { + value: '', + type: outputsObject.type, + }, + }; +} + +export function useWatchFormChange( + id?: string, + form?: UseFormReturn, +) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id) { + values = form?.getValues() || {}; + let nextValues: any = { + ...values, + arguments: convertToObject( + values?.arguments as FormSchemaType['arguments'], + ), + outputs: convertOutputsToObject(values as FormSchemaType), + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} + +export function useHandleLanguageChange( + id?: string, + form?: UseFormReturn, +) { + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + const handleLanguageChange = useCallback( + (lang: string) => { + if (id) { + const script = CodeTemplateStrMap[lang as ProgrammingLanguage]; + form?.setValue('script', script); + form?.setValue( + 'outputs', + (lang === ProgrammingLanguage.Python + ? [] + : {}) as FormSchemaType['outputs'], + ); + updateNodeForm(id, script, ['script']); + } + }, + [form, id, updateNodeForm], + ); + + return handleLanguageChange; +} diff --git a/web/src/pages/data-flow/form/components/api-key-field.tsx b/web/src/pages/data-flow/form/components/api-key-field.tsx new file mode 100644 index 000000000..f9debfc4f --- /dev/null +++ b/web/src/pages/data-flow/form/components/api-key-field.tsx @@ -0,0 +1,32 @@ +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { t } from 'i18next'; +import { useFormContext } from 'react-hook-form'; + +interface IApiKeyFieldProps { + placeholder?: string; +} +export function ApiKeyField({ placeholder }: IApiKeyFieldProps) { + const form = useFormContext(); + return ( + ( + + {t('flow.apiKey')} + + + + + + )} + /> + ); +} diff --git a/web/src/pages/data-flow/form/components/description-field.tsx b/web/src/pages/data-flow/form/components/description-field.tsx new file mode 100644 index 000000000..8fa2eef64 --- /dev/null +++ b/web/src/pages/data-flow/form/components/description-field.tsx @@ -0,0 +1,27 @@ +import { + FormControl, + FormField, + FormItem, + FormLabel, +} from '@/components/ui/form'; +import { Textarea } from '@/components/ui/textarea'; +import { t } from 'i18next'; +import { useFormContext } from 'react-hook-form'; + +export function DescriptionField() { + const form = useFormContext(); + return ( + ( + + {t('flow.description')} + + + + + )} + /> + ); +} diff --git a/web/src/pages/data-flow/form/components/dynamic-input-variable.tsx b/web/src/pages/data-flow/form/components/dynamic-input-variable.tsx new file mode 100644 index 000000000..a5781fd16 --- /dev/null +++ b/web/src/pages/data-flow/form/components/dynamic-input-variable.tsx @@ -0,0 +1,127 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Collapse, Flex, Form, Input, Select } from 'antd'; +import { PropsWithChildren, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; + +import styles from './index.less'; + +interface IProps { + node?: RAGFlowNodeType; +} + +enum VariableType { + Reference = 'reference', + Input = 'input', +} + +const getVariableName = (type: string) => + type === VariableType.Reference ? 'component_id' : 'value'; + +const DynamicVariableForm = ({ node }: IProps) => { + const { t } = useTranslation(); + const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); + const form = Form.useFormInstance(); + + const options = [ + { value: VariableType.Reference, label: t('flow.reference') }, + { value: VariableType.Input, label: t('flow.text') }, + ]; + + const handleTypeChange = useCallback( + (name: number) => () => { + setTimeout(() => { + form.setFieldValue(['query', name, 'component_id'], undefined); + form.setFieldValue(['query', name, 'value'], undefined); + }, 0); + }, + [form], + ); + + return ( + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + + + + + + {({ getFieldValue }) => { + const type = getFieldValue(['query', name, 'type']); + return ( + + {type === VariableType.Reference ? ( + + ) : ( + + )} + + ); + }} + + remove(name)} /> + + ))} + + + + + )} + + ); +}; + +export function FormCollapse({ + children, + title, +}: PropsWithChildren<{ title: string }>) { + return ( + {title}, + children, + }, + ]} + /> + ); +} + +const DynamicInputVariable = ({ node }: IProps) => { + const { t } = useTranslation(); + return ( + + + + ); +}; + +export default DynamicInputVariable; diff --git a/web/src/pages/data-flow/form/components/form-wrapper.tsx b/web/src/pages/data-flow/form/components/form-wrapper.tsx new file mode 100644 index 000000000..d1985b18e --- /dev/null +++ b/web/src/pages/data-flow/form/components/form-wrapper.tsx @@ -0,0 +1,16 @@ +type FormProps = React.ComponentProps<'form'>; + +export function FormWrapper({ children, ...props }: FormProps) { + return ( +
    { + e.preventDefault(); + }} + {...props} + > + {children} +
    + ); +} diff --git a/web/src/pages/data-flow/form/components/index.less b/web/src/pages/data-flow/form/components/index.less new file mode 100644 index 000000000..344514d9e --- /dev/null +++ b/web/src/pages/data-flow/form/components/index.less @@ -0,0 +1,22 @@ +.dynamicInputVariable { + background-color: #ebe9e950; + :global(.ant-collapse-content) { + background-color: #f6f6f657; + } + margin-bottom: 20px; + .title { + font-weight: 600; + font-size: 16px; + } + .variableType { + width: 30%; + } + .variableValue { + flex: 1; + } + + .addButton { + color: rgb(22, 119, 255); + font-weight: 600; + } +} diff --git a/web/src/pages/data-flow/form/components/next-dynamic-input-variable.tsx b/web/src/pages/data-flow/form/components/next-dynamic-input-variable.tsx new file mode 100644 index 000000000..8b4cbd8a9 --- /dev/null +++ b/web/src/pages/data-flow/form/components/next-dynamic-input-variable.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { SideDown } from '@/assets/icon/next-icon'; +import { Button } from '@/components/ui/button'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible'; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { Plus, Trash2 } from 'lucide-react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; + +interface IProps { + node?: RAGFlowNodeType; +} + +enum VariableType { + Reference = 'reference', + Input = 'input', +} + +const getVariableName = (type: string) => + type === VariableType.Reference ? 'component_id' : 'value'; + +export function DynamicVariableForm({ node }: IProps) { + const { t } = useTranslation(); + const form = useFormContext(); + const { fields, remove, append } = useFieldArray({ + name: 'query', + control: form.control, + }); + + const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); + + const options = [ + { value: VariableType.Reference, label: t('flow.reference') }, + { value: VariableType.Input, label: t('flow.text') }, + ]; + + return ( +
    + {fields.map((field, index) => { + const typeField = `query.${index}.type`; + const typeValue = form.watch(typeField); + return ( +
    + ( + + + + { + field.onChange(val); + form.resetField(`query.${index}.value`); + form.resetField(`query.${index}.component_id`); + }} + > + + + + )} + /> + ( + + + + {typeValue === VariableType.Reference ? ( + + ) : ( + + )} + + + + )} + /> + remove(index)} + /> +
    + ); + })} + +
    + ); +} + +export function DynamicInputVariable({ node }: IProps) { + const { t } = useTranslation(); + + return ( + + + + {t('flow.input')} + + + + + + + + ); +} diff --git a/web/src/pages/data-flow/form/components/output.tsx b/web/src/pages/data-flow/form/components/output.tsx new file mode 100644 index 000000000..c481c7a38 --- /dev/null +++ b/web/src/pages/data-flow/form/components/output.tsx @@ -0,0 +1,35 @@ +import { t } from 'i18next'; + +export type OutputType = { + title: string; + type?: string; +}; + +type OutputProps = { + list: Array; +}; + +export function transferOutputs(outputs: Record) { + return Object.entries(outputs).map(([key, value]) => ({ + title: key, + type: value?.type, + })); +} + +export function Output({ list }: OutputProps) { + return ( +
    +
    {t('flow.output')}
    +
      + {list.map((x, idx) => ( +
    • + {x.title}: {x.type} +
    • + ))} +
    +
    + ); +} diff --git a/web/src/pages/data-flow/form/components/prompt-editor/constant.ts b/web/src/pages/data-flow/form/components/prompt-editor/constant.ts new file mode 100644 index 000000000..b6cf30ed9 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/constant.ts @@ -0,0 +1 @@ +export const ProgrammaticTag = 'programmatic'; diff --git a/web/src/pages/data-flow/form/components/prompt-editor/index.css b/web/src/pages/data-flow/form/components/prompt-editor/index.css new file mode 100644 index 000000000..8f3050647 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/index.css @@ -0,0 +1,76 @@ +.typeahead-popover { + background: #fff; + box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3); + border-radius: 8px; + position: fixed; + z-index: 1000; +} + +.typeahead-popover ul { + list-style: none; + margin: 0; + max-height: 200px; + overflow-y: scroll; +} + +.typeahead-popover ul::-webkit-scrollbar { + display: none; +} + +.typeahead-popover ul { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.typeahead-popover ul li { + margin: 0; + min-width: 180px; + font-size: 14px; + outline: none; + cursor: pointer; + border-radius: 8px; +} + +.typeahead-popover ul li.selected { + background: #eee; +} + +.typeahead-popover li { + margin: 0 8px 0 8px; + color: #050505; + cursor: pointer; + line-height: 16px; + font-size: 15px; + display: flex; + align-content: center; + flex-direction: row; + flex-shrink: 0; + background-color: #fff; + border: 0; +} + +.typeahead-popover li.active { + display: flex; + width: 20px; + height: 20px; + background-size: contain; +} + +.typeahead-popover li .text { + display: flex; + line-height: 20px; + flex-grow: 1; + min-width: 150px; +} + +.typeahead-popover li .icon { + display: flex; + width: 20px; + height: 20px; + user-select: none; + margin-right: 8px; + line-height: 16px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} diff --git a/web/src/pages/data-flow/form/components/prompt-editor/index.tsx b/web/src/pages/data-flow/form/components/prompt-editor/index.tsx new file mode 100644 index 000000000..caf6914b4 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/index.tsx @@ -0,0 +1,181 @@ +import { CodeHighlightNode, CodeNode } from '@lexical/code'; +import { + InitialConfigType, + LexicalComposer, +} from '@lexical/react/LexicalComposer'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { HeadingNode, QuoteNode } from '@lexical/rich-text'; +import { + $getRoot, + $getSelection, + EditorState, + Klass, + LexicalNode, +} from 'lexical'; + +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { Variable } from 'lucide-react'; +import { ReactNode, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PasteHandlerPlugin } from './paste-handler-plugin'; +import theme from './theme'; +import { VariableNode } from './variable-node'; +import { VariableOnChangePlugin } from './variable-on-change-plugin'; +import VariablePickerMenuPlugin from './variable-picker-plugin'; + +// Catch any errors that occur during Lexical updates and log them +// or throw them as needed. If you don't throw them, Lexical will +// try to recover gracefully without losing user data. +function onError(error: Error) { + console.error(error); +} + +const Nodes: Array> = [ + HeadingNode, + QuoteNode, + CodeHighlightNode, + CodeNode, + VariableNode, +]; + +type PromptContentProps = { showToolbar?: boolean; multiLine?: boolean }; + +type IProps = { + value?: string; + onChange?: (value?: string) => void; + placeholder?: ReactNode; +} & PromptContentProps; + +function PromptContent({ + showToolbar = true, + multiLine = true, +}: PromptContentProps) { + const [editor] = useLexicalComposerContext(); + const [isBlur, setIsBlur] = useState(false); + const { t } = useTranslation(); + + const insertTextAtCursor = useCallback(() => { + editor.update(() => { + const selection = $getSelection(); + + if (selection !== null) { + selection.insertText(' /'); + } + }); + }, [editor]); + + const handleVariableIconClick = useCallback(() => { + insertTextAtCursor(); + }, [insertTextAtCursor]); + + const handleBlur = useCallback(() => { + setIsBlur(true); + }, []); + + const handleFocus = useCallback(() => { + setIsBlur(false); + }, []); + + return ( +
    + {showToolbar && ( +
    + + + + + + + +

    {t('flow.insertVariableTip')}

    +
    +
    +
    + )} + +
    + ); +} + +export function PromptEditor({ + value, + onChange, + placeholder, + showToolbar, + multiLine = true, +}: IProps) { + const { t } = useTranslation(); + const initialConfig: InitialConfigType = { + namespace: 'PromptEditor', + theme, + onError, + nodes: Nodes, + }; + + const onValueChange = useCallback( + (editorState: EditorState) => { + editorState?.read(() => { + // const listNodes = $nodesOfType(VariableNode); // to be removed + // const allNodes = $dfs(); + + const text = $getRoot().getTextContent(); + + onChange?.(text); + }); + }, + [onChange], + ); + + return ( +
    + + + } + placeholder={ +
    + {placeholder || t('common.promptPlaceholder')} +
    + } + ErrorBoundary={LexicalErrorBoundary} + /> + + + +
    +
    + ); +} diff --git a/web/src/pages/data-flow/form/components/prompt-editor/paste-handler-plugin.tsx b/web/src/pages/data-flow/form/components/prompt-editor/paste-handler-plugin.tsx new file mode 100644 index 000000000..a45a5e5fb --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/paste-handler-plugin.tsx @@ -0,0 +1,83 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { + $createParagraphNode, + $createTextNode, + $getSelection, + $isRangeSelection, + PASTE_COMMAND, +} from 'lexical'; +import { useEffect } from 'react'; + +function PasteHandlerPlugin() { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + const removeListener = editor.registerCommand( + PASTE_COMMAND, + (clipboardEvent: ClipboardEvent) => { + const clipboardData = clipboardEvent.clipboardData; + if (!clipboardData) { + return false; + } + + const text = clipboardData.getData('text/plain'); + if (!text) { + return false; + } + + // Check if text contains line breaks + if (text.includes('\n')) { + editor.update(() => { + const selection = $getSelection(); + if (selection && $isRangeSelection(selection)) { + // Normalize line breaks, merge multiple consecutive line breaks into a single line break + const normalizedText = text.replace(/\n{2,}/g, '\n'); + + // Clear current selection + selection.removeText(); + + // Create a paragraph node to contain all content + const paragraph = $createParagraphNode(); + + // Split text by line breaks + const lines = normalizedText.split('\n'); + + // Process each line + lines.forEach((lineText, index) => { + // Add line text (if any) + if (lineText) { + const textNode = $createTextNode(lineText); + paragraph.append(textNode); + } + + // If not the last line, add a line break + if (index < lines.length - 1) { + const lineBreak = $createTextNode('\n'); + paragraph.append(lineBreak); + } + }); + + // Insert paragraph + selection.insertNodes([paragraph]); + } + }); + + // Prevent default paste behavior + clipboardEvent.preventDefault(); + return true; + } + + // If no line breaks, use default behavior + return false; + }, + 4, + ); + + return () => { + removeListener(); + }; + }, [editor]); + + return null; +} + +export { PasteHandlerPlugin }; diff --git a/web/src/pages/data-flow/form/components/prompt-editor/theme.ts b/web/src/pages/data-flow/form/components/prompt-editor/theme.ts new file mode 100644 index 000000000..1cc2bc155 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/theme.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export default { + code: 'editor-code', + heading: { + h1: 'editor-heading-h1', + h2: 'editor-heading-h2', + h3: 'editor-heading-h3', + h4: 'editor-heading-h4', + h5: 'editor-heading-h5', + }, + image: 'editor-image', + link: 'editor-link', + list: { + listitem: 'editor-listitem', + nested: { + listitem: 'editor-nested-listitem', + }, + ol: 'editor-list-ol', + ul: 'editor-list-ul', + }, + ltr: 'ltr', + paragraph: 'editor-paragraph', + placeholder: 'editor-placeholder', + quote: 'editor-quote', + rtl: 'rtl', + text: { + bold: 'editor-text-bold', + code: 'editor-text-code', + hashtag: 'editor-text-hashtag', + italic: 'editor-text-italic', + overflowed: 'editor-text-overflowed', + strikethrough: 'editor-text-strikethrough', + underline: 'editor-text-underline', + underlineStrikethrough: 'editor-text-underlineStrikethrough', + }, +}; diff --git a/web/src/pages/data-flow/form/components/prompt-editor/variable-node.tsx b/web/src/pages/data-flow/form/components/prompt-editor/variable-node.tsx new file mode 100644 index 000000000..177c370c9 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/variable-node.tsx @@ -0,0 +1,91 @@ +import { BeginId } from '@/pages/flow/constant'; +import { DecoratorNode, LexicalNode, NodeKey } from 'lexical'; +import { ReactNode } from 'react'; +const prefix = BeginId + '@'; + +export class VariableNode extends DecoratorNode { + __value: string; + __label: string; + key?: NodeKey; + __parentLabel?: string | ReactNode; + __icon?: ReactNode; + + static getType(): string { + return 'variable'; + } + + static clone(node: VariableNode): VariableNode { + return new VariableNode( + node.__value, + node.__label, + node.__key, + node.__parentLabel, + node.__icon, + ); + } + + constructor( + value: string, + label: string, + key?: NodeKey, + parent?: string | ReactNode, + icon?: ReactNode, + ) { + super(key); + this.__value = value; + this.__label = label; + this.__parentLabel = parent; + this.__icon = icon; + } + + createDOM(): HTMLElement { + const dom = document.createElement('span'); + dom.className = 'mr-1'; + + return dom; + } + + updateDOM(): false { + return false; + } + + decorate(): ReactNode { + let content: ReactNode = ( +
    {this.__label}
    + ); + if (this.__parentLabel) { + content = ( +
    +
    {this.__icon}
    +
    {this.__parentLabel}
    +
    /
    + {content} +
    + ); + } + return ( +
    + {content} +
    + ); + } + + getTextContent(): string { + return `{${this.__value}}`; + } +} + +export function $createVariableNode( + value: string, + label: string, + parentLabel: string | ReactNode, + icon?: ReactNode, +): VariableNode { + return new VariableNode(value, label, undefined, parentLabel, icon); +} + +export function $isVariableNode( + node: LexicalNode | null | undefined, +): node is VariableNode { + return node instanceof VariableNode; +} diff --git a/web/src/pages/data-flow/form/components/prompt-editor/variable-on-change-plugin.tsx b/web/src/pages/data-flow/form/components/prompt-editor/variable-on-change-plugin.tsx new file mode 100644 index 000000000..86fa66db4 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/variable-on-change-plugin.tsx @@ -0,0 +1,35 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { EditorState, LexicalEditor } from 'lexical'; +import { useEffect } from 'react'; +import { ProgrammaticTag } from './constant'; + +interface IProps { + onChange: ( + editorState: EditorState, + editor?: LexicalEditor, + tags?: Set, + ) => void; +} + +export function VariableOnChangePlugin({ onChange }: IProps) { + // Access the editor through the LexicalComposerContext + const [editor] = useLexicalComposerContext(); + // Wrap our listener in useEffect to handle the teardown and avoid stale references. + useEffect(() => { + // most listeners return a teardown function that can be called to clean them up. + return editor.registerUpdateListener( + ({ editorState, tags, dirtyElements }) => { + // Check if there is a "programmatic" tag + const isProgrammaticUpdate = tags.has(ProgrammaticTag); + + // The onchange event is only triggered when the data is manually updated + // Otherwise, the content will be displayed incorrectly. + if (dirtyElements.size > 0 && !isProgrammaticUpdate) { + onChange(editorState); + } + }, + ); + }, [editor, onChange]); + + return null; +} diff --git a/web/src/pages/data-flow/form/components/prompt-editor/variable-picker-plugin.tsx b/web/src/pages/data-flow/form/components/prompt-editor/variable-picker-plugin.tsx new file mode 100644 index 000000000..f429981c7 --- /dev/null +++ b/web/src/pages/data-flow/form/components/prompt-editor/variable-picker-plugin.tsx @@ -0,0 +1,297 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { + LexicalTypeaheadMenuPlugin, + MenuOption, + useBasicTypeaheadTriggerMatch, +} from '@lexical/react/LexicalTypeaheadMenuPlugin'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $getSelection, + $isRangeSelection, + TextNode, +} from 'lexical'; +import React, { + ReactElement, + ReactNode, + useCallback, + useEffect, + useRef, +} from 'react'; +import * as ReactDOM from 'react-dom'; + +import { $createVariableNode } from './variable-node'; + +import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query'; +import { ProgrammaticTag } from './constant'; +import './index.css'; +class VariableInnerOption extends MenuOption { + label: string; + value: string; + parentLabel: string | JSX.Element; + icon?: ReactNode; + + constructor( + label: string, + value: string, + parentLabel: string | JSX.Element, + icon?: ReactNode, + ) { + super(value); + this.label = label; + this.value = value; + this.parentLabel = parentLabel; + this.icon = icon; + } +} + +class VariableOption extends MenuOption { + label: ReactElement | string; + title: string; + options: VariableInnerOption[]; + + constructor( + label: ReactElement | string, + title: string, + options: VariableInnerOption[], + ) { + super(title); + this.label = label; + this.title = title; + this.options = options; + } +} + +function VariablePickerMenuItem({ + index, + option, + selectOptionAndCleanUp, +}: { + index: number; + option: VariableOption; + selectOptionAndCleanUp: ( + option: VariableOption | VariableInnerOption, + ) => void; +}) { + return ( +
  • +
    + {option.title} +
      + {option.options.map((x) => ( +
    • selectOptionAndCleanUp(x)} + className="hover:bg-slate-300 p-1" + > + {x.label} +
    • + ))} +
    +
    +
  • + ); +} + +export default function VariablePickerMenuPlugin({ + value, +}: { + value?: string; +}): JSX.Element { + const [editor] = useLexicalComposerContext(); + const isFirstRender = useRef(true); + + const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { + minLength: 0, + }); + + const [queryString, setQueryString] = React.useState(''); + + const options = useBuildQueryVariableOptions(); + + const buildNextOptions = useCallback(() => { + let filteredOptions = options; + if (queryString) { + const lowerQuery = queryString.toLowerCase(); + filteredOptions = options + .map((x) => ({ + ...x, + options: x.options.filter( + (y) => + y.label.toLowerCase().includes(lowerQuery) || + y.value.toLowerCase().includes(lowerQuery), + ), + })) + .filter((x) => x.options.length > 0); + } + + const nextOptions: VariableOption[] = filteredOptions.map( + (x) => + new VariableOption( + x.label, + x.title, + x.options.map((y) => { + return new VariableInnerOption(y.label, y.value, x.label, y.icon); + }), + ), + ); + return nextOptions; + }, [options, queryString]); + + const findItemByValue = useCallback( + (value: string) => { + const children = options.reduce< + Array<{ + label: string; + value: string; + parentLabel?: string | ReactNode; + icon?: ReactNode; + }> + >((pre, cur) => { + return pre.concat(cur.options); + }, []); + + return children.find((x) => x.value === value); + }, + [options], + ); + + const onSelectOption = useCallback( + ( + selectedOption: VariableOption | VariableInnerOption, + nodeToRemove: TextNode | null, + closeMenu: () => void, + ) => { + editor.update(() => { + const selection = $getSelection(); + + if (!$isRangeSelection(selection) || selectedOption === null) { + return; + } + + if (nodeToRemove) { + nodeToRemove.remove(); + } + const variableNode = $createVariableNode( + (selectedOption as VariableInnerOption).value, + selectedOption.label as string, + selectedOption.parentLabel as string | ReactNode, + selectedOption.icon as ReactNode, + ); + selection.insertNodes([variableNode]); + + closeMenu(); + }); + }, + [editor], + ); + + const parseTextToVariableNodes = useCallback( + (text: string) => { + const paragraph = $createParagraphNode(); + + // Regular expression to match content within {} + const regex = /{([^}]*)}/g; + let match; + let lastIndex = 0; + while ((match = regex.exec(text)) !== null) { + const { 1: content, index, 0: template } = match; + + // Add the previous text part (if any) + if (index > lastIndex) { + const textNode = $createTextNode(text.slice(lastIndex, index)); + + paragraph.append(textNode); + } + + // Add variable node or text node + const nodeItem = findItemByValue(content); + + if (nodeItem) { + paragraph.append( + $createVariableNode( + content, + nodeItem.label, + nodeItem.parentLabel, + nodeItem.icon, + ), + ); + } else { + paragraph.append($createTextNode(template)); + } + + // Update index + lastIndex = regex.lastIndex; + } + + // Add the last part of text (if any) + if (lastIndex < text.length) { + const textNode = $createTextNode(text.slice(lastIndex)); + paragraph.append(textNode); + } + + $getRoot().clear().append(paragraph); + + if ($isRangeSelection($getSelection())) { + $getRoot().selectEnd(); + } + }, + [findItemByValue], + ); + + useEffect(() => { + if (editor && value && isFirstRender.current) { + isFirstRender.current = false; + editor.update( + () => { + parseTextToVariableNodes(value); + }, + { tag: ProgrammaticTag }, + ); + } + }, [parseTextToVariableNodes, editor, value]); + + return ( + + onQueryChange={setQueryString} + onSelectOption={onSelectOption} + triggerFn={checkForTriggerMatch} + options={buildNextOptions()} + menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { + const nextOptions = buildNextOptions(); + return anchorElementRef.current && nextOptions.length + ? ReactDOM.createPortal( +
    +
      + {nextOptions.map((option, i: number) => ( + + ))} +
    +
    , + anchorElementRef.current, + ) + : null; + }} + /> + ); +} diff --git a/web/src/pages/data-flow/form/components/query-variable.tsx b/web/src/pages/data-flow/form/components/query-variable.tsx new file mode 100644 index 000000000..dafcb4cde --- /dev/null +++ b/web/src/pages/data-flow/form/components/query-variable.tsx @@ -0,0 +1,66 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { toLower } from 'lodash'; +import { ReactNode, useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { VariableType } from '../../constant'; +import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; + +type QueryVariableProps = { + name?: string; + type?: VariableType; + label?: ReactNode; +}; + +export function QueryVariable({ + name = 'query', + type, + label, +}: QueryVariableProps) { + const { t } = useTranslation(); + const form = useFormContext(); + + const nextOptions = useBuildQueryVariableOptions(); + + const finalOptions = useMemo(() => { + return type + ? nextOptions.map((x) => { + return { + ...x, + options: x.options.filter((y) => toLower(y.type).includes(type)), + }; + }) + : nextOptions; + }, [nextOptions, type]); + + return ( + ( + + {label || ( + + {t('flow.query')} + + )} + + + + + + )} + /> + ); +} diff --git a/web/src/pages/data-flow/form/crawler-form/index.tsx b/web/src/pages/data-flow/form/crawler-form/index.tsx new file mode 100644 index 000000000..8c8da6b08 --- /dev/null +++ b/web/src/pages/data-flow/form/crawler-form/index.tsx @@ -0,0 +1,105 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialCrawlerValues } from '../../constant'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { CrawlerResultOptions } from '../../options'; +import { QueryVariable } from '../components/query-variable'; + +export function CrawlerProxyFormField() { + const { t } = useTranslate('flow'); + const form = useFormContext(); + + return ( + ( + + {t('proxy')} + + + + + + )} + /> + ); +} + +export function CrawlerExtractTypeFormField() { + const { t } = useTranslate('flow'); + const form = useFormContext(); + const crawlerResultOptions = useMemo(() => { + return CrawlerResultOptions.map((x) => ({ + value: x, + label: t(`crawlerResultOptions.${x}`), + })); + }, [t]); + + return ( + ( + + {t('extractType')} + + + + + + )} + /> + ); +} + +export const CrawlerFormSchema = { + proxy: z.string().url(), + extract_type: z.string(), +}; + +const FormSchema = z.object({ + query: z.string().optional(), + ...CrawlerFormSchema, +}); + +function CrawlerForm({ node }: INextOperatorForm) { + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: initialCrawlerValues, + mode: 'onChange', + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + { + e.preventDefault(); + }} + > + + + +
    + + ); +} + +export default memo(CrawlerForm); diff --git a/web/src/pages/data-flow/form/deepl-form/index.tsx b/web/src/pages/data-flow/form/deepl-form/index.tsx new file mode 100644 index 000000000..55240b3c1 --- /dev/null +++ b/web/src/pages/data-flow/form/deepl-form/index.tsx @@ -0,0 +1,36 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { useBuildSortOptions } from '../../form-hooks'; +import { IOperatorForm } from '../../interface'; +import { DeepLSourceLangOptions, DeepLTargetLangOptions } from '../../options'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const options = useBuildSortOptions(); + + return ( +
    + + + + + + + + + + + +
    + ); +}; + +export default DeepLForm; diff --git a/web/src/pages/data-flow/form/duckduckgo-form/index.tsx b/web/src/pages/data-flow/form/duckduckgo-form/index.tsx new file mode 100644 index 000000000..776635d3c --- /dev/null +++ b/web/src/pages/data-flow/form/duckduckgo-form/index.tsx @@ -0,0 +1,91 @@ +import { FormContainer } from '@/components/form-container'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { Channel, initialDuckValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const DuckDuckGoFormPartialSchema = { + top_n: z.string(), + channel: z.string(), +}; + +const FormSchema = z.object({ + query: z.string(), + ...DuckDuckGoFormPartialSchema, +}); + +export function DuckDuckGoWidgets() { + const { t } = useTranslate('flow'); + const form = useFormContext(); + + const options = useMemo(() => { + return Object.values(Channel).map((x) => ({ value: x, label: t(x) })); + }, [t]); + + return ( + <> + + ( + + {t('channel')} + + + + + + )} + /> + + ); +} + +const outputList = buildOutputList(initialDuckValues.outputs); + +function DuckDuckGoForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialDuckValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + +
    + +
    +
    + ); +} + +export default memo(DuckDuckGoForm); diff --git a/web/src/pages/data-flow/form/email-form/index.tsx b/web/src/pages/data-flow/form/email-form/index.tsx new file mode 100644 index 000000000..b142dae76 --- /dev/null +++ b/web/src/pages/data-flow/form/email-form/index.tsx @@ -0,0 +1,161 @@ +import { FormContainer } from '@/components/form-container'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { ReactNode } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialEmailValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { PromptEditor } from '../components/prompt-editor'; + +interface InputFormFieldProps { + name: string; + label: ReactNode; + type?: string; +} + +function InputFormField({ name, label, type }: InputFormFieldProps) { + const form = useFormContext(); + + return ( + ( + + {label} + + + + + + )} + /> + ); +} + +function PromptFormField({ name, label }: InputFormFieldProps) { + const form = useFormContext(); + + return ( + ( + + {label} + + + + + + )} + /> + ); +} +export function EmailFormWidgets() { + const { t } = useTranslate('flow'); + + return ( + <> + + + + + + + ); +} + +export const EmailFormPartialSchema = { + smtp_server: z.string(), + smtp_port: z.number(), + email: z.string(), + password: z.string(), + sender_name: z.string(), +}; + +const FormSchema = z.object({ + to_email: z.string(), + cc_email: z.string(), + content: z.string(), + subject: z.string(), + ...EmailFormPartialSchema, +}); + +const outputList = buildOutputList(initialEmailValues.outputs); + +const EmailForm = ({ node }: INextOperatorForm) => { + const { t } = useTranslate('flow'); + const defaultValues = useFormValues(initialEmailValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + + +
    + +
    +
    + ); +}; + +export default EmailForm; diff --git a/web/src/pages/data-flow/form/exesql-form/index.tsx b/web/src/pages/data-flow/form/exesql-form/index.tsx new file mode 100644 index 000000000..5a6cb5ba6 --- /dev/null +++ b/web/src/pages/data-flow/form/exesql-form/index.tsx @@ -0,0 +1,167 @@ +import NumberInput from '@/components/originui/number-input'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { ButtonLoading } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialExeSqlValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { ExeSQLOptions } from '../../options'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; +import { FormSchema, useSubmitForm } from './use-submit-form'; + +const outputList = buildOutputList(initialExeSqlValues.outputs); + +export function ExeSQLFormWidgets({ loading }: { loading: boolean }) { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + return ( + <> + ( + + {t('dbType')} + + + + + + )} + /> + ( + + {t('database')} + + + + + + )} + /> + ( + + {t('username')} + + + + + + )} + /> + ( + + {t('host')} + + + + + + )} + /> + ( + + {t('port')} + + + + + + )} + /> + ( + + {t('password')} + + + + + + )} + /> + + ( + + {t('maxRecords')} + + + + + + )} + /> + +
    + + {t('test')} + +
    + + ); +} + +function ExeSQLForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialExeSqlValues, node); + + const { onSubmit, loading } = useSubmitForm(); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues, + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + +
    + +
    +
    + ); +} + +export default memo(ExeSQLForm); diff --git a/web/src/pages/data-flow/form/exesql-form/use-submit-form.ts b/web/src/pages/data-flow/form/exesql-form/use-submit-form.ts new file mode 100644 index 000000000..8be69c7b0 --- /dev/null +++ b/web/src/pages/data-flow/form/exesql-form/use-submit-form.ts @@ -0,0 +1,31 @@ +import { useTestDbConnect } from '@/hooks/use-agent-request'; +import { useCallback } from 'react'; +import { z } from 'zod'; + +export const ExeSQLFormSchema = { + db_type: z.string().min(1), + database: z.string().min(1), + username: z.string().min(1), + host: z.string().min(1), + port: z.number(), + password: z.string().min(1), + max_records: z.number(), +}; + +export const FormSchema = z.object({ + sql: z.string().optional(), + ...ExeSQLFormSchema, +}); + +export function useSubmitForm() { + const { testDbConnect, loading } = useTestDbConnect(); + + const onSubmit = useCallback( + async (data: z.infer) => { + testDbConnect(data); + }, + [testDbConnect], + ); + + return { loading, onSubmit }; +} diff --git a/web/src/pages/data-flow/form/github-form/index.tsx b/web/src/pages/data-flow/form/github-form/index.tsx new file mode 100644 index 000000000..2e514a3e7 --- /dev/null +++ b/web/src/pages/data-flow/form/github-form/index.tsx @@ -0,0 +1,52 @@ +import { FormContainer } from '@/components/form-container'; +import { TopNFormField } from '@/components/top-n-item'; +import { Form } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { initialGithubValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const FormSchema = z.object({ + query: z.string(), + top_n: z.number(), +}); + +const outputList = buildOutputList(initialGithubValues.outputs); + +function GithubForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialGithubValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + mode: 'onChange', + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + +
    + +
    +
    + ); +} + +export default memo(GithubForm); diff --git a/web/src/pages/data-flow/form/google-form/index.tsx b/web/src/pages/data-flow/form/google-form/index.tsx new file mode 100644 index 000000000..cec5ae4c7 --- /dev/null +++ b/web/src/pages/data-flow/form/google-form/index.tsx @@ -0,0 +1,139 @@ +import { FormContainer } from '@/components/form-container'; +import NumberInput from '@/components/originui/number-input'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialGoogleValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { GoogleCountryOptions, GoogleLanguageOptions } from '../../options'; +import { buildOutputList } from '../../utils/build-output-list'; +import { ApiKeyField } from '../components/api-key-field'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +const outputList = buildOutputList(initialGoogleValues.outputs); + +export const GoogleFormPartialSchema = { + api_key: z.string(), + country: z.string(), + language: z.string(), +}; + +export const FormSchema = z.object({ + ...GoogleFormPartialSchema, + q: z.string(), + start: z.number(), + num: z.number(), +}); + +export function GoogleFormWidgets() { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + return ( + <> + ( + + {t('country')} + + + + + + )} + /> + ( + + {t('language')} + + + + + + )} + /> + + ); +} + +const GoogleForm = ({ node }: INextOperatorForm) => { + const { t } = useTranslate('flow'); + const defaultValues = useFormValues(initialGoogleValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + ( + + {t('flowStart')} + + + + + + )} + /> + ( + + {t('flowNum')} + + + + + + )} + /> + + + +
    + +
    +
    + ); +}; + +export default GoogleForm; diff --git a/web/src/pages/data-flow/form/google-scholar-form/index.tsx b/web/src/pages/data-flow/form/google-scholar-form/index.tsx new file mode 100644 index 000000000..00f54056a --- /dev/null +++ b/web/src/pages/data-flow/form/google-scholar-form/index.tsx @@ -0,0 +1,166 @@ +import { FormContainer } from '@/components/form-container'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Switch } from '@/components/ui/switch'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { DatePicker, DatePickerProps } from 'antd'; +import dayjs from 'dayjs'; +import { memo, useCallback, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialGoogleScholarValues } from '../../constant'; +import { useBuildSortOptions } from '../../form-hooks'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +// TODO: To be replaced +const YearPicker = ({ + onChange, + value, +}: { + onChange?: (val: number | undefined) => void; + value?: number | undefined; +}) => { + const handleChange: DatePickerProps['onChange'] = useCallback( + (val: any) => { + const nextVal = val?.format('YYYY'); + onChange?.(nextVal ? Number(nextVal) : undefined); + }, + [onChange], + ); + // The year needs to be converted into a number and saved to the backend + const nextValue = useMemo(() => { + if (value) { + return dayjs(value.toString()); + } + return undefined; + }, [value]); + + return ; +}; + +export function GoogleScholarFormWidgets() { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + const options = useBuildSortOptions(); + + return ( + <> + + ( + + {t('sortBy')} + + + + + + )} + /> + ( + + {t('yearLow')} + + + + + + )} + /> + ( + + {t('yearHigh')} + + + + + + )} + /> + ( + + {t('patents')} + + + + + + )} + /> + + ); +} + +export const GoogleScholarFormPartialSchema = { + top_n: z.number(), + sort_by: z.string(), + year_low: z.number(), + year_high: z.number(), + patents: z.boolean(), +}; + +export const FormSchema = z.object({ + ...GoogleScholarFormPartialSchema, + query: z.string(), +}); + +const outputList = buildOutputList(initialGoogleScholarValues.outputs); + +function GoogleScholarForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialGoogleScholarValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + +
    + +
    +
    + ); +} + +export default memo(GoogleScholarForm); diff --git a/web/src/pages/data-flow/form/invoke-form/hooks.ts b/web/src/pages/data-flow/form/invoke-form/hooks.ts new file mode 100644 index 000000000..951cd42ae --- /dev/null +++ b/web/src/pages/data-flow/form/invoke-form/hooks.ts @@ -0,0 +1,97 @@ +import get from 'lodash/get'; +import { + ChangeEventHandler, + MouseEventHandler, + useCallback, + useMemo, +} from 'react'; +import { v4 as uuid } from 'uuid'; +import { IGenerateParameter, IInvokeVariable } from '../../interface'; +import useGraphStore from '../../store'; + +export const useHandleOperateParameters = (nodeId: string) => { + const { getNode, updateNodeForm } = useGraphStore((state) => state); + const node = getNode(nodeId); + const dataSource: IGenerateParameter[] = useMemo( + () => get(node, 'data.form.variables', []) as IGenerateParameter[], + [node], + ); + + const changeValue = useCallback( + (row: IInvokeVariable, field: string, value: string) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.id === item.id); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + [field]: value, + }); + + updateNodeForm(nodeId, { variables: newData }); + }, + [dataSource, nodeId, updateNodeForm], + ); + + const handleComponentIdChange = useCallback( + (row: IInvokeVariable) => (value: string) => { + changeValue(row, 'component_id', value); + }, + [changeValue], + ); + + const handleValueChange = useCallback( + (row: IInvokeVariable): ChangeEventHandler => + (e) => { + changeValue(row, 'value', e.target.value); + }, + [changeValue], + ); + + const handleRemove = useCallback( + (id?: string) => () => { + const newData = dataSource.filter((item) => item.id !== id); + updateNodeForm(nodeId, { variables: newData }); + }, + [updateNodeForm, nodeId, dataSource], + ); + + const handleAdd: MouseEventHandler = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + updateNodeForm(nodeId, { + variables: [ + ...dataSource, + { + id: uuid(), + key: '', + component_id: undefined, + value: '', + }, + ], + }); + }, + [dataSource, nodeId, updateNodeForm], + ); + + const handleSave = (row: IGenerateParameter) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.id === item.id); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + ...row, + }); + + updateNodeForm(nodeId, { variables: newData }); + }; + + return { + handleAdd, + handleRemove, + handleComponentIdChange, + handleValueChange, + handleSave, + dataSource, + }; +}; diff --git a/web/src/pages/data-flow/form/invoke-form/index.tsx b/web/src/pages/data-flow/form/invoke-form/index.tsx new file mode 100644 index 000000000..3d67ec030 --- /dev/null +++ b/web/src/pages/data-flow/form/invoke-form/index.tsx @@ -0,0 +1,226 @@ +import { Collapse } from '@/components/collapse'; +import { FormContainer } from '@/components/form-container'; +import NumberInput from '@/components/originui/number-input'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Editor, { loader } from '@monaco-editor/react'; +import { Plus } from 'lucide-react'; +import { memo } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { initialInvokeValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { FormSchema, FormSchemaType } from './schema'; +import { useEditVariableRecord } from './use-edit-variable'; +import { VariableDialog } from './variable-dialog'; +import { VariableTable } from './variable-table'; + +loader.config({ paths: { vs: '/vs' } }); + +enum Method { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', +} + +const MethodOptions = [Method.GET, Method.POST, Method.PUT].map((x) => ({ + label: x, + value: x, +})); + +interface TimeoutInputProps { + value?: number; + onChange?: (value: number | null) => void; +} + +const TimeoutInput = ({ value, onChange }: TimeoutInputProps) => { + const { t } = useTranslation(); + return ( +
    + {t('flow.seconds')} +
    + ); +}; + +const outputList = buildOutputList(initialInvokeValues.outputs); + +function InvokeForm({ node }: INextOperatorForm) { + const { t } = useTranslation(); + const defaultValues = useFormValues(initialInvokeValues, node); + + const form = useForm({ + defaultValues, + resolver: zodResolver(FormSchema), + mode: 'onChange', + }); + + const { + visible, + hideModal, + showModal, + ok, + currentRecord, + otherThanCurrentQuery, + handleDeleteRecord, + } = useEditVariableRecord({ + form, + node, + }); + + const variables = useWatch({ control: form.control, name: 'variables' }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + ( + + {t('flow.url')} + + + + + + )} + /> + ( + + {t('flow.method')} + + + + + + )} + /> + ( + + {t('flow.timeout')} + + + + + + )} + /> + ( + + {t('flow.headers')} + + + + + + )} + /> + ( + + {t('flow.proxy')} + + + + + + )} + /> + ( + + + {t('flow.cleanHtml')} + + + + + + + )} + /> + {/* Create a hidden field to make Form instance record this */} +
    } + /> +
    + {t('flow.parameter')}} + rightContent={ + + } + > + + + {visible && ( + + )} +
    +
    + +
    +
    + ); +} + +export default memo(InvokeForm); diff --git a/web/src/pages/data-flow/form/invoke-form/schema.ts b/web/src/pages/data-flow/form/invoke-form/schema.ts new file mode 100644 index 000000000..a3b11aff2 --- /dev/null +++ b/web/src/pages/data-flow/form/invoke-form/schema.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +export const VariableFormSchema = z.object({ + key: z.string(), + ref: z.string(), + value: z.string(), +}); + +export const FormSchema = z.object({ + url: z.string().url(), + method: z.string(), + timeout: z.number(), + headers: z.string(), + proxy: z.string().url(), + clean_html: z.boolean(), + variables: z.array(VariableFormSchema), +}); + +export type FormSchemaType = z.infer; + +export type VariableFormSchemaType = z.infer; diff --git a/web/src/pages/data-flow/form/invoke-form/use-edit-variable.ts b/web/src/pages/data-flow/form/invoke-form/use-edit-variable.ts new file mode 100644 index 000000000..40b371894 --- /dev/null +++ b/web/src/pages/data-flow/form/invoke-form/use-edit-variable.ts @@ -0,0 +1,70 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import { useSetSelectedRecord } from '@/hooks/logic-hooks'; +import { useCallback, useMemo, useState } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { INextOperatorForm } from '../../interface'; +import { FormSchemaType, VariableFormSchemaType } from './schema'; + +export const useEditVariableRecord = ({ + form, +}: INextOperatorForm & { form: UseFormReturn }) => { + const { setRecord, currentRecord } = + useSetSelectedRecord(); + + const { visible, hideModal, showModal } = useSetModalState(); + const [index, setIndex] = useState(-1); + const variables = useWatch({ + control: form.control, + name: 'variables', + }); + + const otherThanCurrentQuery = useMemo(() => { + return variables.filter((item, idx) => idx !== index); + }, [index, variables]); + + const handleEditRecord = useCallback( + (record: VariableFormSchemaType) => { + const variables = form?.getValues('variables') || []; + + const nextVaribales = + index > -1 + ? variables.toSpliced(index, 1, record) + : [...variables, record]; + + form.setValue('variables', nextVaribales); + + hideModal(); + }, + [form, hideModal, index], + ); + + const handleShowModal = useCallback( + (idx?: number, record?: VariableFormSchemaType) => { + setIndex(idx ?? -1); + setRecord(record ?? ({} as VariableFormSchemaType)); + showModal(); + }, + [setRecord, showModal], + ); + + const handleDeleteRecord = useCallback( + (idx: number) => { + const variables = form?.getValues('variables') || []; + const nextVariables = variables.filter((item, index) => index !== idx); + + form.setValue('variables', nextVariables); + }, + [form], + ); + + return { + ok: handleEditRecord, + currentRecord, + setRecord, + visible, + hideModal, + showModal: handleShowModal, + otherThanCurrentQuery, + handleDeleteRecord, + }; +}; diff --git a/web/src/pages/data-flow/form/invoke-form/variable-dialog.tsx b/web/src/pages/data-flow/form/invoke-form/variable-dialog.tsx new file mode 100644 index 000000000..03c4d83b0 --- /dev/null +++ b/web/src/pages/data-flow/form/invoke-form/variable-dialog.tsx @@ -0,0 +1,143 @@ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { IModalProps } from '@/interfaces/common'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { isEmpty } from 'lodash'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { QueryVariable } from '../components/query-variable'; +import { VariableFormSchemaType } from './schema'; + +type ModalFormProps = { + initialValue: VariableFormSchemaType; + otherThanCurrentQuery: VariableFormSchemaType[]; + submit(values: any): void; +}; + +const FormId = 'BeginParameterForm'; + +function VariableForm({ + initialValue, + otherThanCurrentQuery, + submit, +}: ModalFormProps) { + const { t } = useTranslation(); + const FormSchema = z.object({ + key: z + .string() + .trim() + .min(1) + .refine( + (value) => + !value || !otherThanCurrentQuery.some((x) => x.key === value), + { message: 'The key cannot be repeated!' }, + ), + ref: z.string(), + value: z.string(), + }); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + mode: 'onChange', + defaultValues: { + key: '', + value: '', + ref: '', + }, + }); + + useEffect(() => { + if (!isEmpty(initialValue)) { + form.reset(initialValue); + } + }, [form, initialValue]); + + function onSubmit(data: z.infer) { + submit(data); + } + + return ( +
    + + ( + + {t('flow.key')} + + + + + + )} + /> + + ( + + {t('flow.value')} + + + + + + )} + /> + + + ); +} + +export function VariableDialog({ + initialValue, + hideModal, + otherThanCurrentQuery, + submit, +}: ModalFormProps & IModalProps) { + const { t } = useTranslation(); + + return ( + + + + {t('flow.variableSettings')} + + + + + + + + ); +} diff --git a/web/src/pages/data-flow/form/invoke-form/variable-table.tsx b/web/src/pages/data-flow/form/invoke-form/variable-table.tsx new file mode 100644 index 000000000..33a747670 --- /dev/null +++ b/web/src/pages/data-flow/form/invoke-form/variable-table.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { Pencil, Trash2 } from 'lucide-react'; +import * as React from 'react'; + +import { TableEmpty } from '@/components/table-skeleton'; +import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; +import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query'; +import { VariableFormSchemaType } from './schema'; + +interface IProps { + data: VariableFormSchemaType[]; + deleteRecord(index: number): void; + showModal(index: number, record: VariableFormSchemaType): void; + nodeId?: string; +} + +export function VariableTable({ + data = [], + deleteRecord, + showModal, + nodeId, +}: IProps) { + const { t } = useTranslation(); + const getLabel = useGetVariableLabelByValue(nodeId!); + + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + + const columns: ColumnDef[] = [ + { + accessorKey: 'key', + header: t('flow.key'), + meta: { cellClassName: 'max-w-30' }, + cell: ({ row }) => { + const key: string = row.getValue('key'); + return ( + + +
    {key}
    +
    + +

    {key}

    +
    +
    + ); + }, + }, + { + accessorKey: 'ref', + header: t('flow.ref'), + meta: { cellClassName: 'max-w-30' }, + cell: ({ row }) => { + const ref: string = row.getValue('ref'); + const label = getLabel(ref); + return ( + + +
    {label}
    +
    + +

    {label}

    +
    +
    + ); + }, + }, + { + accessorKey: 'value', + header: t('flow.value'), + cell: ({ row }) =>
    {row.getValue('value')}
    , + }, + { + id: 'actions', + enableHiding: false, + header: t('common.action'), + cell: ({ row }) => { + const record = row.original; + const idx = row.index; + + return ( +
    + + +
    + ); + }, + }, + ]; + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnFilters, + columnVisibility, + }, + }); + + return ( +
    +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + )} + +
    +
    +
    + ); +} diff --git a/web/src/pages/data-flow/form/iteration-form/dynamic-output.tsx b/web/src/pages/data-flow/form/iteration-form/dynamic-output.tsx new file mode 100644 index 000000000..c31be8fd0 --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-form/dynamic-output.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { FormContainer } from '@/components/form-container'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { t } from 'i18next'; +import { X } from 'lucide-react'; +import { ReactNode, useCallback, useMemo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useBuildSubNodeOutputOptions } from './use-build-options'; + +interface IProps { + node?: RAGFlowNodeType; +} + +export function DynamicOutputForm({ node }: IProps) { + const { t } = useTranslation(); + const form = useFormContext(); + const options = useBuildSubNodeOutputOptions(node?.id); + const name = 'outputs'; + + const flatOptions = useMemo(() => { + return options.reduce<{ label: string; value: string; type: string }[]>( + (pre, cur) => { + pre.push(...cur.options); + return pre; + }, + [], + ); + }, [options]); + + const findType = useCallback( + (val: string) => { + const type = flatOptions.find((x) => x.value === val)?.type; + if (type) { + return `Array<${type}>`; + } + }, + [flatOptions], + ); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
    + {fields.map((field, index) => { + const nameField = `${name}.${index}.name`; + const typeField = `${name}.${index}.type`; + return ( +
    + ( + + + + + + + )} + /> + + ( + + + { + form.setValue(typeField, findType(val)); + field.onChange(val); + }} + > + + + + )} + /> +
    } + /> + +
    + ); + })} + append({ name: '', ref: undefined })}> + {t('common.add')} + +
    + ); +} + +export function VariableTitle({ title }: { title: ReactNode }) { + return
    {title}
    ; +} + +export function DynamicOutput({ node }: IProps) { + return ( + + + + + ); +} diff --git a/web/src/pages/data-flow/form/iteration-form/index.tsx b/web/src/pages/data-flow/form/iteration-form/index.tsx new file mode 100644 index 000000000..c70b764fb --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-form/index.tsx @@ -0,0 +1,57 @@ +import { FormContainer } from '@/components/form-container'; +import { Form } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { z } from 'zod'; +import { VariableType } from '../../constant'; +import { INextOperatorForm } from '../../interface'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; +import { DynamicOutput } from './dynamic-output'; +import { OutputArray } from './interface'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-form-change'; + +const FormSchema = z.object({ + query: z.string().optional(), + outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(), +}); + +function IterationForm({ node }: INextOperatorForm) { + const defaultValues = useValues(node); + + const form = useForm({ + defaultValues: defaultValues, + resolver: zodResolver(FormSchema), + }); + + const outputs: OutputArray = useWatch({ + control: form?.control, + name: 'outputs', + }); + + const outputList = useMemo(() => { + return outputs.map((x) => ({ title: x.name, type: x?.type })); + }, [outputs]); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + +
    + ); +} + +export default memo(IterationForm); diff --git a/web/src/pages/data-flow/form/iteration-form/interface.ts b/web/src/pages/data-flow/form/iteration-form/interface.ts new file mode 100644 index 000000000..25f22aab0 --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-form/interface.ts @@ -0,0 +1,2 @@ +export type OutputArray = Array<{ name: string; ref: string; type?: string }>; +export type OutputObject = Record; diff --git a/web/src/pages/data-flow/form/iteration-form/use-build-options.ts b/web/src/pages/data-flow/form/iteration-form/use-build-options.ts new file mode 100644 index 000000000..3439000d4 --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-form/use-build-options.ts @@ -0,0 +1,31 @@ +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { Operator } from '../../constant'; +import { buildOutputOptions } from '../../hooks/use-get-begin-query'; +import useGraphStore from '../../store'; + +export function useBuildSubNodeOutputOptions(nodeId?: string) { + const { nodes } = useGraphStore((state) => state); + + const nodeOutputOptions = useMemo(() => { + if (!nodeId) { + return []; + } + + const subNodeWithOutputList = nodes.filter( + (x) => + x.parentId === nodeId && + x.data.label !== Operator.IterationStart && + !isEmpty(x.data?.form?.outputs), + ); + + return subNodeWithOutputList.map((x) => ({ + label: x.data.name, + value: x.id, + title: x.data.name, + options: buildOutputOptions(x.data.form.outputs, x.id), + })); + }, [nodeId, nodes]); + + return nodeOutputOptions; +} diff --git a/web/src/pages/data-flow/form/iteration-form/use-values.ts b/web/src/pages/data-flow/form/iteration-form/use-values.ts new file mode 100644 index 000000000..29cd06324 --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-form/use-values.ts @@ -0,0 +1,27 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { initialIterationValues } from '../../constant'; +import { OutputObject } from './interface'; + +function convertToArray(outputObject: OutputObject) { + return Object.entries(outputObject).map(([key, value]) => ({ + name: key, + ref: value.ref, + type: value.type, + })); +} + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return { ...initialIterationValues, outputs: [] }; + } + + return { ...formData, outputs: convertToArray(formData.outputs) }; + }, [node?.data?.form]); + + return values; +} diff --git a/web/src/pages/data-flow/form/iteration-form/use-watch-form-change.ts b/web/src/pages/data-flow/form/iteration-form/use-watch-form-change.ts new file mode 100644 index 000000000..4a780667e --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-form/use-watch-form-change.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; +import { OutputArray, OutputObject } from './interface'; + +export function transferToObject(list: OutputArray) { + return list.reduce((pre, cur) => { + pre[cur.name] = { ref: cur.ref, type: cur.type }; + return pre; + }, {}); +} + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + console.log('🚀 ~ useEffect ~ values:', values); + let nextValues: any = { + ...values, + outputs: transferToObject(values.outputs), + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/form/iteration-start-from/index.tsx b/web/src/pages/data-flow/form/iteration-start-from/index.tsx new file mode 100644 index 000000000..ba58b92d1 --- /dev/null +++ b/web/src/pages/data-flow/form/iteration-start-from/index.tsx @@ -0,0 +1,23 @@ +import { Output, OutputType } from '@/pages/agent/form/components/output'; +import { memo } from 'react'; +import { initialIterationStartValues } from '../../constant'; + +const outputs = initialIterationStartValues.outputs; + +const outputList = Object.entries(outputs).reduce( + (pre, [key, value]) => { + pre.push({ title: key, type: value.type }); + + return pre; + }, + [], +); +function IterationStartForm() { + return ( +
    + +
    + ); +} + +export default memo(IterationStartForm); diff --git a/web/src/pages/data-flow/form/jin10-form/index.tsx b/web/src/pages/data-flow/form/jin10-form/index.tsx new file mode 100644 index 000000000..2bc6d774a --- /dev/null +++ b/web/src/pages/data-flow/form/jin10-form/index.tsx @@ -0,0 +1,145 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { IOperatorForm } from '../../interface'; +import { + Jin10CalendarDatashapeOptions, + Jin10CalendarTypeOptions, + Jin10FlashTypeOptions, + Jin10SymbolsDatatypeOptions, + Jin10SymbolsTypeOptions, + Jin10TypeOptions, +} from '../../options'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const jin10TypeOptions = useMemo(() => { + return Jin10TypeOptions.map((x) => ({ + value: x, + label: t(`jin10TypeOptions.${x}`), + })); + }, [t]); + + const jin10FlashTypeOptions = useMemo(() => { + return Jin10FlashTypeOptions.map((x) => ({ + value: x, + label: t(`jin10FlashTypeOptions.${x}`), + })); + }, [t]); + + const jin10CalendarTypeOptions = useMemo(() => { + return Jin10CalendarTypeOptions.map((x) => ({ + value: x, + label: t(`jin10CalendarTypeOptions.${x}`), + })); + }, [t]); + + const jin10CalendarDatashapeOptions = useMemo(() => { + return Jin10CalendarDatashapeOptions.map((x) => ({ + value: x, + label: t(`jin10CalendarDatashapeOptions.${x}`), + })); + }, [t]); + + const jin10SymbolsTypeOptions = useMemo(() => { + return Jin10SymbolsTypeOptions.map((x) => ({ + value: x, + label: t(`jin10SymbolsTypeOptions.${x}`), + })); + }, [t]); + + const jin10SymbolsDatatypeOptions = useMemo(() => { + return Jin10SymbolsDatatypeOptions.map((x) => ({ + value: x, + label: t(`jin10SymbolsDatatypeOptions.${x}`), + })); + }, [t]); + + return ( +
    + + + + + + + + + {({ getFieldValue }) => { + const type = getFieldValue('type'); + switch (type) { + case 'flash': + return ( + <> + + + + + + + + + + + ); + + case 'calendar': + return ( + <> + + + + + + + + ); + + case 'symbols': + return ( + <> + + + + + + + + ); + + case 'news': + return ( + <> + + + + + + + + ); + + default: + return <>; + } + }} + +
    + ); +}; + +export default Jin10Form; diff --git a/web/src/pages/data-flow/form/keyword-extract-form/index.tsx b/web/src/pages/data-flow/form/keyword-extract-form/index.tsx new file mode 100644 index 000000000..bda5d44f5 --- /dev/null +++ b/web/src/pages/data-flow/form/keyword-extract-form/index.tsx @@ -0,0 +1,48 @@ +import { NextLLMSelect } from '@/components/llm-select/next'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useTranslation } from 'react-i18next'; +import { INextOperatorForm } from '../../interface'; +import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; + +const KeywordExtractForm = ({ form, node }: INextOperatorForm) => { + const { t } = useTranslation(); + + return ( +
    + { + e.preventDefault(); + }} + > + + ( + + + {t('chat.model')} + + + + + + + )} + /> + + + + ); +}; + +export default KeywordExtractForm; diff --git a/web/src/pages/data-flow/form/message-form/index.tsx b/web/src/pages/data-flow/form/message-form/index.tsx new file mode 100644 index 000000000..05c831a84 --- /dev/null +++ b/web/src/pages/data-flow/form/message-form/index.tsx @@ -0,0 +1,101 @@ +import { FormContainer } from '@/components/form-container'; +import { BlockButton, Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { X } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { INextOperatorForm } from '../../interface'; +import { FormWrapper } from '../components/form-wrapper'; +import { PromptEditor } from '../components/prompt-editor'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; + +function MessageForm({ node }: INextOperatorForm) { + const { t } = useTranslation(); + + const values = useValues(node); + + const FormSchema = z.object({ + content: z + .array( + z.object({ + value: z.string(), + }), + ) + .optional(), + }); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + const { fields, append, remove } = useFieldArray({ + name: 'content', + control: form.control, + }); + + return ( +
    + + + + {t('flow.msg')} +
    + {fields.map((field, index) => ( +
    + ( + + + {/* */} + + + + )} + /> + {fields.length > 1 && ( + + )} +
    + ))} + + append({ value: '' })} // "" will cause the inability to add, refer to: https://github.com/orgs/react-hook-form/discussions/8485#discussioncomment-2961861 + > + {t('flow.addMessage')} + +
    + +
    +
    +
    +
    + ); +} + +export default memo(MessageForm); diff --git a/web/src/pages/data-flow/form/message-form/use-values.ts b/web/src/pages/data-flow/form/message-form/use-values.ts new file mode 100644 index 000000000..6a90881be --- /dev/null +++ b/web/src/pages/data-flow/form/message-form/use-values.ts @@ -0,0 +1,22 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { initialMessageValues } from '../../constant'; +import { convertToObjectArray } from '../../utils'; + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return initialMessageValues; + } + + return { + ...formData, + content: convertToObjectArray(formData.content), + }; + }, [node]); + + return values; +} diff --git a/web/src/pages/data-flow/form/message-form/use-watch-change.ts b/web/src/pages/data-flow/form/message-form/use-watch-change.ts new file mode 100644 index 000000000..10c35c653 --- /dev/null +++ b/web/src/pages/data-flow/form/message-form/use-watch-change.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; +import { convertToStringArray } from '../../utils'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = values; + + nextValues = { + ...values, + content: convertToStringArray(values.content), + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/form/pubmed-form/index.tsx b/web/src/pages/data-flow/form/pubmed-form/index.tsx new file mode 100644 index 000000000..a2c35d55c --- /dev/null +++ b/web/src/pages/data-flow/form/pubmed-form/index.tsx @@ -0,0 +1,90 @@ +import { FormContainer } from '@/components/form-container'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialPubMedValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const PubMedFormPartialSchema = { + top_n: z.number(), + email: z.string().email(), +}; + +export const FormSchema = z.object({ + ...PubMedFormPartialSchema, + query: z.string(), +}); + +export function PubMedFormWidgets() { + const form = useFormContext(); + const { t } = useTranslate('flow'); + + return ( + <> + + ( + + {t('email')} + + + + + + )} + /> + + ); +} + +const outputList = buildOutputList(initialPubMedValues.outputs); + +function PubMedForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialPubMedValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + mode: 'onChange', + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + +
    + +
    +
    + ); +} + +export default memo(PubMedForm); diff --git a/web/src/pages/data-flow/form/qweather-form/index.tsx b/web/src/pages/data-flow/form/qweather-form/index.tsx new file mode 100644 index 000000000..eee088762 --- /dev/null +++ b/web/src/pages/data-flow/form/qweather-form/index.tsx @@ -0,0 +1,157 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { INextOperatorForm } from '../../interface'; +import { + QWeatherLangOptions, + QWeatherTimePeriodOptions, + QWeatherTypeOptions, + QWeatherUserTypeOptions, +} from '../../options'; +import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; + +enum FormFieldName { + Type = 'type', + UserType = 'user_type', +} + +const QWeatherForm = ({ form, node }: INextOperatorForm) => { + const { t } = useTranslation(); + const typeValue = form.watch(FormFieldName.Type); + + const qWeatherLangOptions = useMemo(() => { + return QWeatherLangOptions.map((x) => ({ + value: x, + label: t(`flow.qWeatherLangOptions.${x}`), + })); + }, [t]); + + const qWeatherTypeOptions = useMemo(() => { + return QWeatherTypeOptions.map((x) => ({ + value: x, + label: t(`flow.qWeatherTypeOptions.${x}`), + })); + }, [t]); + + const qWeatherUserTypeOptions = useMemo(() => { + return QWeatherUserTypeOptions.map((x) => ({ + value: x, + label: t(`flow.qWeatherUserTypeOptions.${x}`), + })); + }, [t]); + + const getQWeatherTimePeriodOptions = useCallback(() => { + let options = QWeatherTimePeriodOptions; + const userType = form.getValues(FormFieldName.UserType); + if (userType === 'free') { + options = options.slice(0, 3); + } + return options.map((x) => ({ + value: x, + label: t(`flow.qWeatherTimePeriodOptions.${x}`), + })); + }, [form, t]); + + return ( +
    + { + e.preventDefault(); + }} + > + + ( + + {t('flow.webApiKey')} + + + + + + )} + /> + ( + + {t('flow.lang')} + + + + + + )} + /> + ( + + {t('flow.type')} + + + + + + )} + /> + ( + + {t('flow.userType')} + + + + + + )} + /> + {typeValue === 'weather' && ( + ( + + {t('flow.timePeriod')} + + + + + + )} + /> + )} + + + ); +}; + +export default QWeatherForm; diff --git a/web/src/pages/data-flow/form/relevant-form/hooks.ts b/web/src/pages/data-flow/form/relevant-form/hooks.ts new file mode 100644 index 000000000..413a0ac38 --- /dev/null +++ b/web/src/pages/data-flow/form/relevant-form/hooks.ts @@ -0,0 +1,41 @@ +import pick from 'lodash/pick'; +import { useCallback, useEffect } from 'react'; +import { IOperatorForm } from '../../interface'; +import useGraphStore from '../../store'; + +export const useBuildRelevantOptions = () => { + const nodes = useGraphStore((state) => state.nodes); + + const buildRelevantOptions = useCallback( + (toList: string[]) => { + return nodes + .filter( + (x) => !toList.some((y) => y === x.id), // filter out selected values ​​in other to fields from the current drop-down box options + ) + .map((x) => ({ label: x.data.name, value: x.id })); + }, + [nodes], + ); + + return buildRelevantOptions; +}; + +/** + * monitor changes in the connection and synchronize the target to the yes and no fields of the form + * similar to the categorize-form's useHandleFormValuesChange method + * @param param0 + */ +export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => { + const getNode = useGraphStore((state) => state.getNode); + const node = getNode(nodeId); + + const watchFormChanges = useCallback(() => { + if (node) { + form?.setFieldsValue(pick(node, ['yes', 'no'])); + } + }, [node, form]); + + useEffect(() => { + watchFormChanges(); + }, [watchFormChanges]); +}; diff --git a/web/src/pages/data-flow/form/relevant-form/index.tsx b/web/src/pages/data-flow/form/relevant-form/index.tsx new file mode 100644 index 000000000..e2366f6f0 --- /dev/null +++ b/web/src/pages/data-flow/form/relevant-form/index.tsx @@ -0,0 +1,49 @@ +import LLMSelect from '@/components/llm-select'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { Operator } from '../../constant'; +import { useBuildFormSelectOptions } from '../../form-hooks'; +import { IOperatorForm } from '../../interface'; +import { useWatchConnectionChanges } from './hooks'; + +const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const buildRelevantOptions = useBuildFormSelectOptions( + Operator.Relevant, + node?.id, + ); + useWatchConnectionChanges({ nodeId: node?.id, form }); + + return ( +
    + + + + + + +
    + ); +}; + +export default RelevantForm; diff --git a/web/src/pages/data-flow/form/retrieval-form/next.tsx b/web/src/pages/data-flow/form/retrieval-form/next.tsx new file mode 100644 index 000000000..425d08e4b --- /dev/null +++ b/web/src/pages/data-flow/form/retrieval-form/next.tsx @@ -0,0 +1,126 @@ +import { Collapse } from '@/components/collapse'; +import { CrossLanguageFormField } from '@/components/cross-language-form-field'; +import { FormContainer } from '@/components/form-container'; +import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { RerankFormFields } from '@/components/rerank'; +import { SimilaritySliderFormField } from '@/components/similarity-slider'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Textarea } from '@/components/ui/textarea'; +import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { initialRetrievalValues } from '../../constant'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { PromptEditor } from '../components/prompt-editor'; +import { useValues } from './use-values'; + +export const RetrievalPartialSchema = { + similarity_threshold: z.coerce.number(), + keywords_similarity_weight: z.coerce.number(), + top_n: z.coerce.number(), + top_k: z.coerce.number(), + kb_ids: z.array(z.string()), + rerank_id: z.string(), + empty_response: z.string(), + cross_languages: z.array(z.string()), + use_kg: z.boolean(), +}; + +export const FormSchema = z.object({ + query: z.string().optional(), + ...RetrievalPartialSchema, +}); + +export function EmptyResponseField() { + const { t } = useTranslation(); + const form = useFormContext(); + + return ( + ( + + + {t('chat.emptyResponse')} + + + + + + + )} + /> + + {/* Create a hidden field to make Form instance record this */} +
    } + /> + + {t('flow.input')} + + + } + rightContent={ + + } + > + + + + {visible && ( + + )} + + + + ); +} + +export default memo(UserFillUpForm); diff --git a/web/src/pages/data-flow/form/user-fill-up-form/use-values.ts b/web/src/pages/data-flow/form/user-fill-up-form/use-values.ts new file mode 100644 index 000000000..0af1c78c3 --- /dev/null +++ b/web/src/pages/data-flow/form/user-fill-up-form/use-values.ts @@ -0,0 +1,21 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { initialUserFillUpValues } from '../../constant'; +import { buildBeginInputListFromObject } from '../begin-form/utils'; + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return initialUserFillUpValues; + } + + const inputs = buildBeginInputListFromObject(formData?.inputs); + + return { ...(formData || {}), inputs }; + }, [node?.data?.form]); + + return values; +} diff --git a/web/src/pages/data-flow/form/user-fill-up-form/use-watch-change.ts b/web/src/pages/data-flow/form/user-fill-up-form/use-watch-change.ts new file mode 100644 index 000000000..bdf4b9a91 --- /dev/null +++ b/web/src/pages/data-flow/form/user-fill-up-form/use-watch-change.ts @@ -0,0 +1,35 @@ +import { omit } from 'lodash'; +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { BeginQuery } from '../../interface'; +import useGraphStore from '../../store'; + +function transferInputsArrayToObject(inputs: BeginQuery[] = []) { + return inputs.reduce>>((pre, cur) => { + pre[cur.key] = omit(cur, 'key'); + + return pre; + }, {}); +} + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // TODO: This should only be executed when the form changes + if (id) { + values = form?.getValues() || {}; + + const inputs = transferInputsArrayToObject(values.inputs); + + const nextValues = { + ...values, + inputs, + outputs: inputs, + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/form/wencai-form/index.tsx b/web/src/pages/data-flow/form/wencai-form/index.tsx new file mode 100644 index 000000000..a4e668ed9 --- /dev/null +++ b/web/src/pages/data-flow/form/wencai-form/index.tsx @@ -0,0 +1,97 @@ +import { FormContainer } from '@/components/form-container'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo, useMemo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { initialWenCaiValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { WenCaiQueryTypeOptions } from '../../options'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const WenCaiPartialSchema = { + top_n: z.number(), + query_type: z.string(), +}; + +export const FormSchema = z.object({ + ...WenCaiPartialSchema, + query: z.string(), +}); + +export function WenCaiFormWidgets() { + const { t } = useTranslation(); + const form = useFormContext(); + + const wenCaiQueryTypeOptions = useMemo(() => { + return WenCaiQueryTypeOptions.map((x) => ({ + value: x, + label: t(`flow.wenCaiQueryTypeOptions.${x}`), + })); + }, [t]); + + return ( + <> + + ( + + {t('flow.queryType')} + + + + + + )} + /> + + ); +} + +const outputList = buildOutputList(initialWenCaiValues.outputs); + +function WenCaiForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialWenCaiValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + +
    + +
    +
    + ); +} + +export default memo(WenCaiForm); diff --git a/web/src/pages/data-flow/form/wikipedia-form/index.tsx b/web/src/pages/data-flow/form/wikipedia-form/index.tsx new file mode 100644 index 000000000..1603d802e --- /dev/null +++ b/web/src/pages/data-flow/form/wikipedia-form/index.tsx @@ -0,0 +1,88 @@ +import { FormContainer } from '@/components/form-container'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { TopNFormField } from '@/components/top-n-item'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialWikipediaValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { LanguageOptions } from '../../options'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const WikipediaFormPartialSchema = { + top_n: z.string(), + language: z.string(), +}; + +const FormSchema = z.object({ + query: z.string(), + ...WikipediaFormPartialSchema, +}); + +export function WikipediaFormWidgets() { + const { t } = useTranslate('common'); + const form = useFormContext(); + + return ( + <> + + ( + + {t('language')} + + + + + + )} + /> + + ); +} + +const outputList = buildOutputList(initialWikipediaValues.outputs); + +function WikipediaForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialWikipediaValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + +
    + +
    +
    + ); +} + +export default memo(WikipediaForm); diff --git a/web/src/pages/data-flow/form/yahoo-finance-form/index.tsx b/web/src/pages/data-flow/form/yahoo-finance-form/index.tsx new file mode 100644 index 000000000..68a34836b --- /dev/null +++ b/web/src/pages/data-flow/form/yahoo-finance-form/index.tsx @@ -0,0 +1,125 @@ +import { FormContainer } from '@/components/form-container'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Switch } from '@/components/ui/switch'; +import { useTranslate } from '@/hooks/common-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { ReactNode } from 'react'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import { initialYahooFinanceValues } from '../../constant'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; +import { FormWrapper } from '../components/form-wrapper'; +import { Output } from '../components/output'; +import { QueryVariable } from '../components/query-variable'; + +export const YahooFinanceFormPartialSchema = { + info: z.boolean(), + history: z.boolean(), + financials: z.boolean(), + balance_sheet: z.boolean(), + cash_flow_statement: z.boolean(), + news: z.boolean(), +}; + +const FormSchema = z.object({ + stock_code: z.string(), + ...YahooFinanceFormPartialSchema, +}); + +interface SwitchFormFieldProps { + name: string; + label: ReactNode; +} +function SwitchFormField({ name, label }: SwitchFormFieldProps) { + const form = useFormContext(); + + return ( + ( + + {label} + + + + + + )} + /> + ); +} + +export function YahooFinanceFormWidgets() { + const { t } = useTranslate('flow'); + return ( + <> + + + + + + + + + + ); +} + +const outputList = buildOutputList(initialYahooFinanceValues.outputs); + +const YahooFinanceForm = ({ node }: INextOperatorForm) => { + const { t } = useTranslate('flow'); + + const defaultValues = useFormValues(initialYahooFinanceValues, node); + + const form = useForm>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + + + + +
    + +
    +
    + ); +}; + +export default YahooFinanceForm; diff --git a/web/src/pages/data-flow/hooks.tsx b/web/src/pages/data-flow/hooks.tsx new file mode 100644 index 000000000..5d1537623 --- /dev/null +++ b/web/src/pages/data-flow/hooks.tsx @@ -0,0 +1,405 @@ +import { + Connection, + Edge, + getOutgoers, + Node, + Position, + ReactFlowInstance, +} from '@xyflow/react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +// import { shallow } from 'zustand/shallow'; +import { useFetchModelId } from '@/hooks/logic-hooks'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { humanId } from 'human-id'; +import { get, lowerFirst } from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { + initialAgentValues, + initialAkShareValues, + initialArXivValues, + initialBaiduFanyiValues, + initialBaiduValues, + initialBeginValues, + initialBingValues, + initialCategorizeValues, + initialCodeValues, + initialConcentratorValues, + initialCrawlerValues, + initialDeepLValues, + initialDuckValues, + initialEmailValues, + initialExeSqlValues, + initialGithubValues, + initialGoogleScholarValues, + initialGoogleValues, + initialInvokeValues, + initialIterationValues, + initialJin10Values, + initialKeywordExtractValues, + initialMessageValues, + initialNoteValues, + initialPubMedValues, + initialQWeatherValues, + initialRelevantValues, + initialRetrievalValues, + initialRewriteQuestionValues, + initialSearXNGValues, + initialStringTransformValues, + initialSwitchValues, + initialTavilyExtractValues, + initialTavilyValues, + initialTuShareValues, + initialUserFillUpValues, + initialWaitingDialogueValues, + initialWenCaiValues, + initialWikipediaValues, + initialYahooFinanceValues, + NodeMap, + Operator, + RestrictedUpstreamMap, +} from './constant'; +import useGraphStore, { RFState } from './store'; +import { + generateNodeNamesWithIncreasingIndex, + getNodeDragHandle, + getRelativePositionToIterationNode, + replaceIdWithText, +} from './utils'; + +const selector = (state: RFState) => ({ + nodes: state.nodes, + edges: state.edges, + onNodesChange: state.onNodesChange, + onEdgesChange: state.onEdgesChange, + onConnect: state.onConnect, + setNodes: state.setNodes, + onSelectionChange: state.onSelectionChange, + onEdgeMouseEnter: state.onEdgeMouseEnter, + onEdgeMouseLeave: state.onEdgeMouseLeave, +}); + +export const useSelectCanvasData = () => { + // return useStore(useShallow(selector)); // throw error + // return useStore(selector, shallow); + return useGraphStore(selector); +}; + +export const useInitializeOperatorParams = () => { + const llmId = useFetchModelId(); + + const initialFormValuesMap = useMemo(() => { + return { + [Operator.Begin]: initialBeginValues, + [Operator.Retrieval]: initialRetrievalValues, + [Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId }, + [Operator.Relevant]: { ...initialRelevantValues, llm_id: llmId }, + [Operator.RewriteQuestion]: { + ...initialRewriteQuestionValues, + llm_id: llmId, + }, + [Operator.Message]: initialMessageValues, + [Operator.KeywordExtract]: { + ...initialKeywordExtractValues, + llm_id: llmId, + }, + [Operator.DuckDuckGo]: initialDuckValues, + [Operator.Baidu]: initialBaiduValues, + [Operator.Wikipedia]: initialWikipediaValues, + [Operator.PubMed]: initialPubMedValues, + [Operator.ArXiv]: initialArXivValues, + [Operator.Google]: initialGoogleValues, + [Operator.Bing]: initialBingValues, + [Operator.GoogleScholar]: initialGoogleScholarValues, + [Operator.DeepL]: initialDeepLValues, + [Operator.SearXNG]: initialSearXNGValues, + [Operator.GitHub]: initialGithubValues, + [Operator.BaiduFanyi]: initialBaiduFanyiValues, + [Operator.QWeather]: initialQWeatherValues, + [Operator.ExeSQL]: { ...initialExeSqlValues, llm_id: llmId }, + [Operator.Switch]: initialSwitchValues, + [Operator.WenCai]: initialWenCaiValues, + [Operator.AkShare]: initialAkShareValues, + [Operator.YahooFinance]: initialYahooFinanceValues, + [Operator.Jin10]: initialJin10Values, + [Operator.Concentrator]: initialConcentratorValues, + [Operator.TuShare]: initialTuShareValues, + [Operator.Note]: initialNoteValues, + [Operator.Crawler]: initialCrawlerValues, + [Operator.Invoke]: initialInvokeValues, + [Operator.Email]: initialEmailValues, + [Operator.Iteration]: initialIterationValues, + [Operator.IterationStart]: initialIterationValues, + [Operator.Code]: initialCodeValues, + [Operator.WaitingDialogue]: initialWaitingDialogueValues, + [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, + [Operator.TavilySearch]: initialTavilyValues, + [Operator.TavilyExtract]: initialTavilyExtractValues, + [Operator.Tool]: {}, + [Operator.UserFillUp]: initialUserFillUpValues, + [Operator.StringTransform]: initialStringTransformValues, + }; + }, [llmId]); + + const initializeOperatorParams = useCallback( + (operatorName: Operator) => { + return initialFormValuesMap[operatorName]; + }, + [initialFormValuesMap], + ); + + return initializeOperatorParams; +}; + +export const useHandleDrag = () => { + const handleDragStart = useCallback( + (operatorId: string) => (ev: React.DragEvent) => { + ev.dataTransfer.setData('application/@xyflow/react', operatorId); + ev.dataTransfer.effectAllowed = 'move'; + }, + [], + ); + + return { handleDragStart }; +}; + +export const useGetNodeName = () => { + const { t } = useTranslation(); + + return (type: string) => { + const name = t(`flow.${lowerFirst(type)}`); + return name; + }; +}; + +export const useHandleDrop = () => { + const addNode = useGraphStore((state) => state.addNode); + const nodes = useGraphStore((state) => state.nodes); + const [reactFlowInstance, setReactFlowInstance] = + useState>(); + const initializeOperatorParams = useInitializeOperatorParams(); + const getNodeName = useGetNodeName(); + + const onDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }, []); + + const onDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + + const type = event.dataTransfer.getData('application/@xyflow/react'); + + // check if the dropped element is valid + if (typeof type === 'undefined' || !type) { + return; + } + + // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition + // and you don't need to subtract the reactFlowBounds.left/top anymore + // details: https://@xyflow/react.dev/whats-new/2023-11-10 + const position = reactFlowInstance?.screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + const newNode: Node = { + id: `${type}:${humanId()}`, + type: NodeMap[type as Operator] || 'ragNode', + position: position || { + x: 0, + y: 0, + }, + data: { + label: `${type}`, + name: generateNodeNamesWithIncreasingIndex(getNodeName(type), nodes), + form: initializeOperatorParams(type as Operator), + }, + sourcePosition: Position.Right, + targetPosition: Position.Left, + dragHandle: getNodeDragHandle(type), + }; + + if (type === Operator.Iteration) { + newNode.width = 500; + newNode.height = 250; + const iterationStartNode: Node = { + id: `${Operator.IterationStart}:${humanId()}`, + type: 'iterationStartNode', + position: { x: 50, y: 100 }, + // draggable: false, + data: { + label: Operator.IterationStart, + name: Operator.IterationStart, + form: {}, + }, + parentId: newNode.id, + extent: 'parent', + }; + addNode(newNode); + addNode(iterationStartNode); + } else { + const subNodeOfIteration = getRelativePositionToIterationNode( + nodes, + position, + ); + if (subNodeOfIteration) { + newNode.parentId = subNodeOfIteration.parentId; + newNode.position = subNodeOfIteration.position; + newNode.extent = 'parent'; + } + addNode(newNode); + } + }, + [reactFlowInstance, getNodeName, nodes, initializeOperatorParams, addNode], + ); + + return { onDrop, onDragOver, setReactFlowInstance, reactFlowInstance }; +}; + +export const useValidateConnection = () => { + const { getOperatorTypeFromId, getParentIdById, edges, nodes } = + useGraphStore((state) => state); + + const isSameNodeChild = useCallback( + (connection: Connection | Edge) => { + const sourceParentId = getParentIdById(connection.source); + const targetParentId = getParentIdById(connection.target); + if (sourceParentId || targetParentId) { + return sourceParentId === targetParentId; + } + return true; + }, + [getParentIdById], + ); + + const hasCanvasCycle = useCallback( + (connection: Connection | Edge) => { + const target = nodes.find((node) => node.id === connection.target); + const hasCycle = (node: RAGFlowNodeType, visited = new Set()) => { + if (visited.has(node.id)) return false; + + visited.add(node.id); + + for (const outgoer of getOutgoers(node, nodes, edges)) { + if (outgoer.id === connection.source) return true; + if (hasCycle(outgoer, visited)) return true; + } + }; + + if (target?.id === connection.source) return false; + + return target ? !hasCycle(target) : false; + }, + [edges, nodes], + ); + + // restricted lines cannot be connected successfully. + const isValidConnection = useCallback( + (connection: Connection | Edge) => { + // node cannot connect to itself + const isSelfConnected = connection.target === connection.source; + + // limit the connection between two nodes to only one connection line in one direction + // const hasLine = edges.some( + // (x) => x.source === connection.source && x.target === connection.target, + // ); + + const ret = + !isSelfConnected && + RestrictedUpstreamMap[ + getOperatorTypeFromId(connection.source) as Operator + ]?.every((x) => x !== getOperatorTypeFromId(connection.target)) && + isSameNodeChild(connection) && + hasCanvasCycle(connection); + return ret; + }, + [getOperatorTypeFromId, hasCanvasCycle, isSameNodeChild], + ); + + return isValidConnection; +}; + +export const useReplaceIdWithName = () => { + const getNode = useGraphStore((state) => state.getNode); + + const replaceIdWithName = useCallback( + (id?: string) => { + return getNode(id)?.data.name; + }, + [getNode], + ); + + return replaceIdWithName; +}; + +export const useReplaceIdWithText = (output: unknown) => { + const getNameById = useReplaceIdWithName(); + + return { + replacedOutput: replaceIdWithText(output, getNameById), + getNameById, + }; +}; + +export const useDuplicateNode = () => { + const duplicateNodeById = useGraphStore((store) => store.duplicateNode); + const getNodeName = useGetNodeName(); + + const duplicateNode = useCallback( + (id: string, label: string) => { + duplicateNodeById(id, getNodeName(label)); + }, + [duplicateNodeById, getNodeName], + ); + + return duplicateNode; +}; + +export const useCopyPaste = () => { + const nodes = useGraphStore((state) => state.nodes); + const duplicateNode = useDuplicateNode(); + + const onCopyCapture = useCallback( + (event: ClipboardEvent) => { + if (get(event, 'srcElement.tagName') !== 'BODY') return; + + event.preventDefault(); + const nodesStr = JSON.stringify( + nodes.filter((n) => n.selected && n.data.label !== Operator.Begin), + ); + + event.clipboardData?.setData('agent:nodes', nodesStr); + }, + [nodes], + ); + + const onPasteCapture = useCallback( + (event: ClipboardEvent) => { + const nodes = JSON.parse( + event.clipboardData?.getData('agent:nodes') || '[]', + ) as RAGFlowNodeType[] | undefined; + + if (Array.isArray(nodes) && nodes.length) { + event.preventDefault(); + nodes.forEach((n) => { + duplicateNode(n.id, n.data.label); + }); + } + }, + [duplicateNode], + ); + + useEffect(() => { + window.addEventListener('copy', onCopyCapture); + return () => { + window.removeEventListener('copy', onCopyCapture); + }; + }, [onCopyCapture]); + + useEffect(() => { + window.addEventListener('paste', onPasteCapture); + return () => { + window.removeEventListener('paste', onPasteCapture); + }; + }, [onPasteCapture]); +}; diff --git a/web/src/pages/data-flow/hooks/use-add-node.ts b/web/src/pages/data-flow/hooks/use-add-node.ts new file mode 100644 index 000000000..625eeb5cd --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-add-node.ts @@ -0,0 +1,462 @@ +import { useFetchModelId } from '@/hooks/logic-hooks'; +import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react'; +import humanId from 'human-id'; +import { t } from 'i18next'; +import { lowerFirst } from 'lodash'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + NodeHandleId, + NodeMap, + Operator, + initialAgentValues, + initialAkShareValues, + initialArXivValues, + initialBaiduFanyiValues, + initialBaiduValues, + initialBeginValues, + initialBingValues, + initialCategorizeValues, + initialCodeValues, + initialConcentratorValues, + initialCrawlerValues, + initialDeepLValues, + initialDuckValues, + initialEmailValues, + initialExeSqlValues, + initialGithubValues, + initialGoogleScholarValues, + initialGoogleValues, + initialInvokeValues, + initialIterationStartValues, + initialIterationValues, + initialJin10Values, + initialKeywordExtractValues, + initialMessageValues, + initialNoteValues, + initialPubMedValues, + initialQWeatherValues, + initialRelevantValues, + initialRetrievalValues, + initialRewriteQuestionValues, + initialSearXNGValues, + initialStringTransformValues, + initialSwitchValues, + initialTavilyExtractValues, + initialTavilyValues, + initialTuShareValues, + initialUserFillUpValues, + initialWaitingDialogueValues, + initialWenCaiValues, + initialWikipediaValues, + initialYahooFinanceValues, +} from '../constant'; +import useGraphStore from '../store'; +import { + generateNodeNamesWithIncreasingIndex, + getNodeDragHandle, +} from '../utils'; + +function isBottomSubAgent(type: string, position: Position) { + return ( + (type === Operator.Agent && position === Position.Bottom) || + type === Operator.Tool + ); +} +export const useInitializeOperatorParams = () => { + const llmId = useFetchModelId(); + + const initialFormValuesMap = useMemo(() => { + return { + [Operator.Begin]: initialBeginValues, + [Operator.Retrieval]: initialRetrievalValues, + [Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId }, + [Operator.Relevant]: { ...initialRelevantValues, llm_id: llmId }, + [Operator.RewriteQuestion]: { + ...initialRewriteQuestionValues, + llm_id: llmId, + }, + [Operator.Message]: initialMessageValues, + [Operator.KeywordExtract]: { + ...initialKeywordExtractValues, + llm_id: llmId, + }, + [Operator.DuckDuckGo]: initialDuckValues, + [Operator.Baidu]: initialBaiduValues, + [Operator.Wikipedia]: initialWikipediaValues, + [Operator.PubMed]: initialPubMedValues, + [Operator.ArXiv]: initialArXivValues, + [Operator.Google]: initialGoogleValues, + [Operator.Bing]: initialBingValues, + [Operator.GoogleScholar]: initialGoogleScholarValues, + [Operator.DeepL]: initialDeepLValues, + [Operator.SearXNG]: initialSearXNGValues, + [Operator.GitHub]: initialGithubValues, + [Operator.BaiduFanyi]: initialBaiduFanyiValues, + [Operator.QWeather]: initialQWeatherValues, + [Operator.ExeSQL]: initialExeSqlValues, + [Operator.Switch]: initialSwitchValues, + [Operator.WenCai]: initialWenCaiValues, + [Operator.AkShare]: initialAkShareValues, + [Operator.YahooFinance]: initialYahooFinanceValues, + [Operator.Jin10]: initialJin10Values, + [Operator.Concentrator]: initialConcentratorValues, + [Operator.TuShare]: initialTuShareValues, + [Operator.Note]: initialNoteValues, + [Operator.Crawler]: initialCrawlerValues, + [Operator.Invoke]: initialInvokeValues, + [Operator.Email]: initialEmailValues, + [Operator.Iteration]: initialIterationValues, + [Operator.IterationStart]: initialIterationStartValues, + [Operator.Code]: initialCodeValues, + [Operator.WaitingDialogue]: initialWaitingDialogueValues, + [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, + [Operator.Tool]: {}, + [Operator.TavilySearch]: initialTavilyValues, + [Operator.UserFillUp]: initialUserFillUpValues, + [Operator.StringTransform]: initialStringTransformValues, + [Operator.TavilyExtract]: initialTavilyExtractValues, + }; + }, [llmId]); + + const initializeOperatorParams = useCallback( + (operatorName: Operator, position: Position) => { + const initialValues = initialFormValuesMap[operatorName]; + if (isBottomSubAgent(operatorName, position)) { + return { + ...initialValues, + description: t('flow.descriptionMessage'), + user_prompt: t('flow.userPromptDefaultValue'), + }; + } + + return initialValues; + }, + [initialFormValuesMap], + ); + + return { initializeOperatorParams, initialFormValuesMap }; +}; + +export const useGetNodeName = () => { + const { t } = useTranslation(); + + return (type: string) => { + const name = t(`flow.${lowerFirst(type)}`); + return name; + }; +}; + +export function useCalculateNewlyChildPosition() { + const getNode = useGraphStore((state) => state.getNode); + const nodes = useGraphStore((state) => state.nodes); + const edges = useGraphStore((state) => state.edges); + + const calculateNewlyBackChildPosition = useCallback( + (id?: string, sourceHandle?: string) => { + const parentNode = getNode(id); + + // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes + const allChildNodeIds = edges + .filter((x) => x.source === id && x.sourceHandle === sourceHandle) + .map((x) => x.target); + + const yAxises = nodes + .filter((x) => allChildNodeIds.some((y) => y === x.id)) + .map((x) => x.position.y); + + const maxY = Math.max(...yAxises); + + const position = { + y: yAxises.length > 0 ? maxY + 150 : parentNode?.position.y || 0, + x: (parentNode?.position.x || 0) + 300, + }; + + return position; + }, + [edges, getNode, nodes], + ); + + return { calculateNewlyBackChildPosition }; +} + +function useAddChildEdge() { + const addEdge = useGraphStore((state) => state.addEdge); + + const addChildEdge = useCallback( + (position: Position = Position.Right, edge: Partial) => { + if ( + position === Position.Right && + edge.source && + edge.target && + edge.sourceHandle + ) { + addEdge({ + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: NodeHandleId.End, + }); + } + }, + [addEdge], + ); + + return { addChildEdge }; +} + +function useAddToolNode() { + const { nodes, edges, addEdge, getNode, addNode } = useGraphStore( + (state) => state, + ); + + const addToolNode = useCallback( + (newNode: Node, nodeId?: string): boolean => { + const agentNode = getNode(nodeId); + + if (agentNode) { + const childToolNodeIds = edges + .filter( + (x) => x.source === nodeId && x.sourceHandle === NodeHandleId.Tool, + ) + .map((x) => x.target); + + if ( + childToolNodeIds.length > 0 && + nodes.some((x) => x.id === childToolNodeIds[0]) + ) { + return false; + } + + newNode.position = { + x: agentNode.position.x - 82, + y: agentNode.position.y + 140, + }; + + addNode(newNode); + if (nodeId) { + addEdge({ + source: nodeId, + target: newNode.id, + sourceHandle: NodeHandleId.Tool, + targetHandle: NodeHandleId.End, + }); + } + return true; + } + return false; + }, + [addEdge, addNode, edges, getNode, nodes], + ); + + return { addToolNode }; +} + +function useResizeIterationNode() { + const { getNode, nodes, updateNode } = useGraphStore((state) => state); + + const resizeIterationNode = useCallback( + (type: string, position: Position, parentId?: string) => { + const parentNode = getNode(parentId); + if (parentNode && !isBottomSubAgent(type, position)) { + const MoveRightDistance = 310; + const childNodeList = nodes.filter((x) => x.parentId === parentId); + const maxX = Math.max(...childNodeList.map((x) => x.position.x)); + if (maxX + MoveRightDistance > parentNode.position.x) { + updateNode({ + ...parentNode, + width: (parentNode.width || 0) + MoveRightDistance, + position: { + x: parentNode.position.x + MoveRightDistance / 2, + y: parentNode.position.y, + }, + }); + } + } + }, + [getNode, nodes, updateNode], + ); + + return { resizeIterationNode }; +} +type CanvasMouseEvent = Pick< + React.MouseEvent, + 'clientX' | 'clientY' +>; + +export function useAddNode(reactFlowInstance?: ReactFlowInstance) { + const { edges, nodes, addEdge, addNode, getNode } = useGraphStore( + (state) => state, + ); + const getNodeName = useGetNodeName(); + const { initializeOperatorParams } = useInitializeOperatorParams(); + const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); + const { addChildEdge } = useAddChildEdge(); + const { addToolNode } = useAddToolNode(); + const { resizeIterationNode } = useResizeIterationNode(); + // const [reactFlowInstance, setReactFlowInstance] = + // useState>(); + + const addCanvasNode = useCallback( + ( + type: string, + params: { + nodeId?: string; + position: Position; + id?: string; + isFromConnectionDrag?: boolean; + } = { + position: Position.Right, + }, + ) => + (event?: CanvasMouseEvent): string | undefined => { + const nodeId = params.nodeId; + const node = getNode(nodeId); + + // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition + // and you don't need to subtract the reactFlowBounds.left/top anymore + // details: https://@xyflow/react.dev/whats-new/2023-11-10 + let position = reactFlowInstance?.screenToFlowPosition({ + x: event?.clientX || 0, + y: event?.clientY || 0, + }); + + if ( + params.position === Position.Right && + type !== Operator.Note && + !params.isFromConnectionDrag + ) { + position = calculateNewlyBackChildPosition(nodeId, params.id); + } + + const newNode: Node = { + id: `${type}:${humanId()}`, + type: NodeMap[type as Operator] || 'ragNode', + position: position || { + x: 0, + y: 0, + }, + data: { + label: `${type}`, + name: generateNodeNamesWithIncreasingIndex( + getNodeName(type), + nodes, + ), + form: initializeOperatorParams(type as Operator, params.position), + }, + sourcePosition: Position.Right, + targetPosition: Position.Left, + dragHandle: getNodeDragHandle(type), + }; + + if (node && node.parentId) { + newNode.parentId = node.parentId; + newNode.extent = 'parent'; + const parentNode = getNode(node.parentId); + if (parentNode && !isBottomSubAgent(type, params.position)) { + resizeIterationNode(type, params.position, node.parentId); + } + } + + if (type === Operator.Iteration) { + newNode.width = 500; + newNode.height = 250; + const iterationStartNode: Node = { + id: `${Operator.IterationStart}:${humanId()}`, + type: 'iterationStartNode', + position: { x: 50, y: 100 }, + // draggable: false, + data: { + label: Operator.IterationStart, + name: Operator.IterationStart, + form: initialIterationStartValues, + }, + parentId: newNode.id, + extent: 'parent', + }; + addNode(newNode); + addNode(iterationStartNode); + if (nodeId) { + addEdge({ + source: nodeId, + target: newNode.id, + sourceHandle: NodeHandleId.Start, + targetHandle: NodeHandleId.End, + }); + } + return newNode.id; + } else if ( + type === Operator.Agent && + params.position === Position.Bottom + ) { + const agentNode = getNode(nodeId); + if (agentNode) { + // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes + const allChildAgentNodeIds = edges + .filter( + (x) => + x.source === nodeId && + x.sourceHandle === NodeHandleId.AgentBottom, + ) + .map((x) => x.target); + + const xAxises = nodes + .filter((x) => allChildAgentNodeIds.some((y) => y === x.id)) + .map((x) => x.position.x); + + const maxX = Math.max(...xAxises); + + newNode.position = { + x: xAxises.length > 0 ? maxX + 262 : agentNode.position.x + 82, + y: agentNode.position.y + 140, + }; + } + addNode(newNode); + if (nodeId) { + addEdge({ + source: nodeId, + target: newNode.id, + sourceHandle: NodeHandleId.AgentBottom, + targetHandle: NodeHandleId.AgentTop, + }); + } + return newNode.id; + } else if (type === Operator.Tool) { + const toolNodeAdded = addToolNode(newNode, params.nodeId); + return toolNodeAdded ? newNode.id : undefined; + } else { + addNode(newNode); + addChildEdge(params.position, { + source: params.nodeId, + target: newNode.id, + sourceHandle: params.id, + }); + } + + return newNode.id; + }, + [ + addChildEdge, + addEdge, + addNode, + addToolNode, + calculateNewlyBackChildPosition, + edges, + getNode, + getNodeName, + initializeOperatorParams, + nodes, + reactFlowInstance, + resizeIterationNode, + ], + ); + + const addNoteNode = useCallback( + (e: CanvasMouseEvent) => { + addCanvasNode(Operator.Note)(e); + }, + [addCanvasNode], + ); + + return { addCanvasNode, addNoteNode }; +} diff --git a/web/src/pages/data-flow/hooks/use-agent-tool-initial-values.ts b/web/src/pages/data-flow/hooks/use-agent-tool-initial-values.ts new file mode 100644 index 000000000..05864184d --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-agent-tool-initial-values.ts @@ -0,0 +1,70 @@ +import { omit, pick } from 'lodash'; +import { useCallback } from 'react'; +import { Operator } from '../constant'; +import { useInitializeOperatorParams } from './use-add-node'; + +export function useAgentToolInitialValues() { + const { initialFormValuesMap } = useInitializeOperatorParams(); + + const initializeAgentToolValues = useCallback( + (operatorName: Operator) => { + const initialValues = initialFormValuesMap[operatorName]; + + switch (operatorName) { + case Operator.Retrieval: + return { + ...omit(initialValues, 'query'), + description: '', + }; + case (Operator.TavilySearch, Operator.TavilyExtract): + return { + api_key: '', + }; + case Operator.ExeSQL: + return omit(initialValues, 'sql'); + case Operator.Bing: + return omit(initialValues, 'query'); + case Operator.YahooFinance: + return omit(initialValues, 'stock_code'); + + case Operator.Email: + return pick( + initialValues, + 'smtp_server', + 'smtp_port', + 'email', + 'password', + 'sender_name', + ); + + case Operator.DuckDuckGo: + return pick(initialValues, 'top_n', 'channel'); + + case Operator.Wikipedia: + return pick(initialValues, 'top_n', 'language'); + case Operator.Google: + return pick(initialValues, 'api_key', 'country', 'language'); + case Operator.GoogleScholar: + return omit(initialValues, 'query', 'outputs'); + case Operator.ArXiv: + return pick(initialValues, 'top_n', 'sort_by'); + case Operator.PubMed: + return pick(initialValues, 'top_n', 'email'); + case Operator.GitHub: + return pick(initialValues, 'top_n'); + case Operator.WenCai: + return pick(initialValues, 'top_n', 'query_type'); + case Operator.Code: + return {}; + case Operator.SearXNG: + return pick(initialValues, 'searxng_url', 'top_n'); + + default: + return initialValues; + } + }, + [initialFormValuesMap], + ); + + return { initializeAgentToolValues }; +} diff --git a/web/src/pages/data-flow/hooks/use-before-delete.tsx b/web/src/pages/data-flow/hooks/use-before-delete.tsx new file mode 100644 index 000000000..d08333c86 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-before-delete.tsx @@ -0,0 +1,82 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { Node, OnBeforeDelete } from '@xyflow/react'; +import { Operator } from '../constant'; +import useGraphStore from '../store'; +import { deleteAllDownstreamAgentsAndTool } from '../utils/delete-node'; + +const UndeletableNodes = [Operator.Begin, Operator.IterationStart]; + +export function useBeforeDelete() { + const { getOperatorTypeFromId, getNode } = useGraphStore((state) => state); + + const agentPredicate = (node: Node) => { + return getOperatorTypeFromId(node.id) === Operator.Agent; + }; + + const handleBeforeDelete: OnBeforeDelete = async ({ + nodes, // Nodes to be deleted + edges, // Edges to be deleted + }) => { + const toBeDeletedNodes = nodes.filter((node) => { + const operatorType = node.data?.label as Operator; + if (operatorType === Operator.Begin) { + return false; + } + + if ( + operatorType === Operator.IterationStart && + !nodes.some((x) => x.id === node.parentId) + ) { + return false; + } + + return true; + }); + + const toBeDeletedEdges = edges.filter((edge) => { + const sourceType = getOperatorTypeFromId(edge.source) as Operator; + const downStreamNodes = nodes.filter((x) => x.id === edge.target); + + // This edge does not need to be deleted, the range of edges that do not need to be deleted is smaller, so consider the case where it does not need to be deleted + if ( + UndeletableNodes.includes(sourceType) && // Upstream node is Begin or IterationStart + downStreamNodes.length === 0 // Downstream node does not exist in the nodes to be deleted + ) { + if (!nodes.some((x) => x.id === edge.source)) { + return true; // Can be deleted + } + return false; // Cannot be deleted + } + + return true; + }); + + // Delete the agent and tool nodes downstream of the agent node + if (nodes.some(agentPredicate)) { + nodes.filter(agentPredicate).forEach((node) => { + const { downstreamAgentAndToolEdges, downstreamAgentAndToolNodeIds } = + deleteAllDownstreamAgentsAndTool(node.id, edges); + + downstreamAgentAndToolNodeIds.forEach((nodeId) => { + const currentNode = getNode(nodeId); + if (toBeDeletedNodes.every((x) => x.id !== nodeId) && currentNode) { + toBeDeletedNodes.push(currentNode); + } + }); + + downstreamAgentAndToolEdges.forEach((edge) => { + if (toBeDeletedEdges.every((x) => x.id !== edge.id)) { + toBeDeletedEdges.push(edge); + } + }); + }, []); + } + + return { + nodes: toBeDeletedNodes, + edges: toBeDeletedEdges, + }; + }; + + return { handleBeforeDelete }; +} diff --git a/web/src/pages/data-flow/hooks/use-build-dsl.ts b/web/src/pages/data-flow/hooks/use-build-dsl.ts new file mode 100644 index 000000000..eb32b2317 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-build-dsl.ts @@ -0,0 +1,29 @@ +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { useCallback } from 'react'; +import useGraphStore from '../store'; +import { buildDslComponentsByGraph } from '../utils'; + +export const useBuildDslData = () => { + const { data } = useFetchAgent(); + const { nodes, edges } = useGraphStore((state) => state); + + const buildDslData = useCallback( + (currentNodes?: RAGFlowNodeType[]) => { + const dslComponents = buildDslComponentsByGraph( + currentNodes ?? nodes, + edges, + data.dsl.components, + ); + + return { + ...data.dsl, + graph: { nodes: currentNodes ?? nodes, edges }, + components: dslComponents, + }; + }, + [data.dsl, edges, nodes], + ); + + return { buildDslData }; +}; diff --git a/web/src/pages/data-flow/hooks/use-cache-chat-log.ts b/web/src/pages/data-flow/hooks/use-cache-chat-log.ts new file mode 100644 index 000000000..c61079003 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-cache-chat-log.ts @@ -0,0 +1,88 @@ +import { + IEventList, + INodeEvent, + MessageEventType, +} from '@/hooks/use-send-message'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +export const ExcludeTypes = [ + MessageEventType.Message, + MessageEventType.MessageEnd, +]; + +export function useCacheChatLog() { + const [eventList, setEventList] = useState([]); + const [messageIdPool, setMessageIdPool] = useState< + Record + >({}); + + const [currentMessageId, setCurrentMessageId] = useState(''); + useEffect(() => { + setMessageIdPool((prev) => ({ ...prev, [currentMessageId]: eventList })); + }, [currentMessageId, eventList]); + + const filterEventListByMessageId = useCallback( + (messageId: string) => { + return messageIdPool[messageId]?.filter( + (x) => x.message_id === messageId, + ); + }, + [messageIdPool], + ); + + const filterEventListByEventType = useCallback( + (eventType: string) => { + return messageIdPool[currentMessageId]?.filter( + (x) => x.event === eventType, + ); + }, + [messageIdPool, currentMessageId], + ); + + const clearEventList = useCallback(() => { + setEventList([]); + setMessageIdPool({}); + }, []); + + const addEventList = useCallback((events: IEventList, message_id: string) => { + setEventList((x) => { + const list = [...x, ...events]; + setMessageIdPool((prev) => ({ ...prev, [message_id]: list })); + return list; + }); + }, []); + + const currentEventListWithoutMessage = useMemo(() => { + const list = messageIdPool[currentMessageId]?.filter( + (x) => + x.message_id === currentMessageId && + ExcludeTypes.every((y) => y !== x.event), + ); + return list as INodeEvent[]; + }, [currentMessageId, messageIdPool]); + + const currentEventListWithoutMessageById = useCallback( + (messageId: string) => { + const list = messageIdPool[messageId]?.filter( + (x) => + x.message_id === messageId && + ExcludeTypes.every((y) => y !== x.event), + ); + return list as INodeEvent[]; + }, + [messageIdPool], + ); + + return { + eventList, + currentEventListWithoutMessage, + currentEventListWithoutMessageById, + setEventList, + clearEventList, + addEventList, + filterEventListByEventType, + filterEventListByMessageId, + setCurrentMessageId, + currentMessageId, + }; +} diff --git a/web/src/pages/data-flow/hooks/use-change-node-name.ts b/web/src/pages/data-flow/hooks/use-change-node-name.ts new file mode 100644 index 000000000..61a5653d7 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-change-node-name.ts @@ -0,0 +1,120 @@ +import message from '@/components/ui/message'; +import { trim } from 'lodash'; +import { + ChangeEvent, + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { Operator } from '../constant'; +import useGraphStore from '../store'; +import { getAgentNodeTools } from '../utils'; + +export function useHandleTooNodeNameChange({ + id, + name, + setName, +}: { + id?: string; + name?: string; + setName: Dispatch>; +}) { + const { clickedToolId, findUpstreamNodeById, updateNodeForm } = useGraphStore( + (state) => state, + ); + const agentNode = findUpstreamNodeById(id); + const tools = getAgentNodeTools(agentNode); + + const previousName = useMemo(() => { + const tool = tools.find((x) => x.component_name === clickedToolId); + return tool?.name || tool?.component_name; + }, [clickedToolId, tools]); + + const handleToolNameBlur = useCallback(() => { + const trimmedName = trim(name); + const existsSameName = tools.some((x) => x.name === trimmedName); + if (trimmedName === '' || existsSameName) { + if (existsSameName && previousName !== name) { + message.error('The name cannot be repeated'); + } + setName(previousName || ''); + return; + } + + if (agentNode?.id) { + const nextTools = tools.map((x) => { + if (x.component_name === clickedToolId) { + return { + ...x, + name, + }; + } + return x; + }); + updateNodeForm(agentNode?.id, nextTools, ['tools']); + } + }, [ + agentNode?.id, + clickedToolId, + name, + previousName, + setName, + tools, + updateNodeForm, + ]); + + return { handleToolNameBlur, previousToolName: previousName }; +} + +export const useHandleNodeNameChange = ({ + id, + data, +}: { + id?: string; + data: any; +}) => { + const [name, setName] = useState(''); + const { updateNodeName, nodes, getOperatorTypeFromId } = useGraphStore( + (state) => state, + ); + const previousName = data?.name; + const isToolNode = getOperatorTypeFromId(id) === Operator.Tool; + + const { handleToolNameBlur, previousToolName } = useHandleTooNodeNameChange({ + id, + name, + setName, + }); + + const handleNameBlur = useCallback(() => { + const existsSameName = nodes.some((x) => x.data.name === name); + if (trim(name) === '' || existsSameName) { + if (existsSameName && previousName !== name) { + message.error('The name cannot be repeated'); + } + setName(previousName); + return; + } + + if (id) { + updateNodeName(id, name); + } + }, [name, id, updateNodeName, previousName, nodes]); + + const handleNameChange = useCallback((e: ChangeEvent) => { + setName(e.target.value); + }, []); + + useEffect(() => { + setName(isToolNode ? previousToolName : previousName); + }, [isToolNode, previousName, previousToolName]); + + return { + name, + handleNameBlur: isToolNode ? handleToolNameBlur : handleNameBlur, + handleNameChange, + }; +}; diff --git a/web/src/pages/data-flow/hooks/use-chat-logic.ts b/web/src/pages/data-flow/hooks/use-chat-logic.ts new file mode 100644 index 000000000..42b0533cb --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-chat-logic.ts @@ -0,0 +1,60 @@ +import { MessageType } from '@/constants/chat'; +import { Message } from '@/interfaces/database/chat'; +import { IMessage } from '@/pages/chat/interface'; +import { get } from 'lodash'; +import { useCallback, useMemo } from 'react'; +import { BeginQuery } from '../interface'; +import { buildBeginQueryWithObject } from '../utils'; +type IAwaitCompentData = { + derivedMessages: IMessage[]; + sendFormMessage: (params: { + inputs: Record; + id: string; + }) => void; + canvasId: string; +}; +const useAwaitCompentData = (props: IAwaitCompentData) => { + const { derivedMessages, sendFormMessage, canvasId } = props; + + const getInputs = useCallback((message: Message) => { + return get(message, 'data.inputs', {}) as Record; + }, []); + + const buildInputList = useCallback( + (message: Message) => { + return Object.entries(getInputs(message)).map(([key, val]) => { + return { + ...val, + key, + }; + }); + }, + [getInputs], + ); + + const handleOk = useCallback( + (message: Message) => (values: BeginQuery[]) => { + const inputs = getInputs(message); + const nextInputs = buildBeginQueryWithObject(inputs, values); + sendFormMessage({ + inputs: nextInputs, + id: canvasId, + }); + }, + [getInputs, sendFormMessage, canvasId], + ); + + const isWaitting = useMemo(() => { + const temp = derivedMessages?.some((message, i) => { + const flag = + message.role === MessageType.Assistant && + derivedMessages.length - 1 === i && + message.data; + return flag; + }); + return temp; + }, [derivedMessages]); + return { getInputs, buildInputList, handleOk, isWaitting }; +}; + +export { useAwaitCompentData }; diff --git a/web/src/pages/data-flow/hooks/use-export-json.ts b/web/src/pages/data-flow/hooks/use-export-json.ts new file mode 100644 index 000000000..1efe6bb50 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-export-json.ts @@ -0,0 +1,71 @@ +import { useToast } from '@/components/hooks/use-toast'; +import { FileMimeType, Platform } from '@/constants/common'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { IGraph } from '@/interfaces/database/flow'; +import { downloadJsonFile } from '@/utils/file-util'; +import { message } from 'antd'; +import isEmpty from 'lodash/isEmpty'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBuildDslData } from './use-build-dsl'; +import { useSetGraphInfo } from './use-set-graph'; + +export const useHandleExportOrImportJsonFile = () => { + const { buildDslData } = useBuildDslData(); + const { + visible: fileUploadVisible, + hideModal: hideFileUploadModal, + showModal: showFileUploadModal, + } = useSetModalState(); + const setGraphInfo = useSetGraphInfo(); + const { data } = useFetchAgent(); + const { t } = useTranslation(); + const { toast } = useToast(); + + const onFileUploadOk = useCallback( + async ({ + fileList, + platform, + }: { + fileList: File[]; + platform: Platform; + }) => { + console.log('🚀 ~ useHandleExportOrImportJsonFile ~ platform:', platform); + if (fileList.length > 0) { + const file = fileList[0]; + if (file.type !== FileMimeType.Json) { + toast({ title: t('flow.jsonUploadTypeErrorMessage') }); + return; + } + + const graphStr = await file.text(); + const errorMessage = t('flow.jsonUploadContentErrorMessage'); + try { + const graph = JSON.parse(graphStr); + if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) { + setGraphInfo(graph ?? ({} as IGraph)); + hideFileUploadModal(); + } else { + message.error(errorMessage); + } + } catch (error) { + message.error(errorMessage); + } + } + }, + [hideFileUploadModal, setGraphInfo, t, toast], + ); + + const handleExportJson = useCallback(() => { + downloadJsonFile(buildDslData().graph, `${data.title}.json`); + }, [buildDslData, data.title]); + + return { + fileUploadVisible, + handleExportJson, + handleImportJson: showFileUploadModal, + hideFileUploadModal, + onFileUploadOk, + }; +}; diff --git a/web/src/pages/data-flow/hooks/use-fetch-data.ts b/web/src/pages/data-flow/hooks/use-fetch-data.ts new file mode 100644 index 000000000..5a1ca40cb --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-fetch-data.ts @@ -0,0 +1,19 @@ +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { IGraph } from '@/interfaces/database/flow'; +import { useEffect } from 'react'; +import { useSetGraphInfo } from './use-set-graph'; + +export const useFetchDataOnMount = () => { + const { loading, data, refetch } = useFetchAgent(); + const setGraphInfo = useSetGraphInfo(); + + useEffect(() => { + setGraphInfo(data?.dsl?.graph ?? ({} as IGraph)); + }, [setGraphInfo, data]); + + useEffect(() => { + refetch(); + }, [refetch]); + + return { loading, flowDetail: data }; +}; diff --git a/web/src/pages/data-flow/hooks/use-find-mcp-by-id.ts b/web/src/pages/data-flow/hooks/use-find-mcp-by-id.ts new file mode 100644 index 000000000..e4c5aed5a --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-find-mcp-by-id.ts @@ -0,0 +1,12 @@ +import { useListMcpServer } from '@/hooks/use-mcp-request'; + +export function useFindMcpById() { + const { data } = useListMcpServer(); + + const findMcpById = (id: string) => + data.mcp_servers.find((item) => item.id === id); + + return { + findMcpById, + }; +} diff --git a/web/src/pages/data-flow/hooks/use-form-values.ts b/web/src/pages/data-flow/hooks/use-form-values.ts new file mode 100644 index 000000000..edb2abbbd --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-form-values.ts @@ -0,0 +1,20 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; + +export function useFormValues( + defaultValues: Record, + node?: RAGFlowNodeType, +) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + return formData; + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/data-flow/hooks/use-get-begin-query.tsx b/web/src/pages/data-flow/hooks/use-get-begin-query.tsx new file mode 100644 index 000000000..83cda2078 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-get-begin-query.tsx @@ -0,0 +1,317 @@ +import { AgentGlobals } from '@/constants/agent'; +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { Edge } from '@xyflow/react'; +import { DefaultOptionType } from 'antd/es/select'; +import { t } from 'i18next'; +import { isEmpty } from 'lodash'; +import get from 'lodash/get'; +import { + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { + AgentDialogueMode, + BeginId, + BeginQueryType, + Operator, + VariableType, +} from '../constant'; +import { AgentFormContext } from '../context'; +import { buildBeginInputListFromObject } from '../form/begin-form/utils'; +import { BeginQuery } from '../interface'; +import OperatorIcon from '../operator-icon'; +import useGraphStore from '../store'; + +export function useSelectBeginNodeDataInputs() { + const getNode = useGraphStore((state) => state.getNode); + + return buildBeginInputListFromObject( + getNode(BeginId)?.data?.form?.inputs ?? {}, + ); +} + +export function useIsTaskMode() { + const getNode = useGraphStore((state) => state.getNode); + + return useMemo(() => { + const node = getNode(BeginId); + return node?.data?.form?.mode === AgentDialogueMode.Task; + }, [getNode]); +} + +export const useGetBeginNodeDataQuery = () => { + const getNode = useGraphStore((state) => state.getNode); + + const getBeginNodeDataQuery = useCallback(() => { + return buildBeginInputListFromObject( + get(getNode(BeginId), 'data.form.inputs', {}), + ); + }, [getNode]); + + return getBeginNodeDataQuery; +}; + +export const useGetBeginNodeDataInputs = () => { + const getNode = useGraphStore((state) => state.getNode); + + const inputs = get(getNode(BeginId), 'data.form.inputs', {}); + + const beginNodeDataInputs = useMemo(() => { + return buildBeginInputListFromObject(inputs); + }, [inputs]); + + return beginNodeDataInputs; +}; + +export const useGetBeginNodeDataQueryIsSafe = () => { + const [isBeginNodeDataQuerySafe, setIsBeginNodeDataQuerySafe] = + useState(false); + const inputs = useSelectBeginNodeDataInputs(); + const nodes = useGraphStore((state) => state.nodes); + + useEffect(() => { + const query: BeginQuery[] = inputs; + const isSafe = !query.some((q) => !q.optional && q.type === 'file'); + setIsBeginNodeDataQuerySafe(isSafe); + }, [inputs, nodes]); + + return isBeginNodeDataQuerySafe; +}; + +function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) { + return nodeIds.reduce((pre, nodeId) => { + const currentEdges = edges.filter((x) => x.target === nodeId); + + const upstreamNodeIds: string[] = currentEdges.map((x) => x.source); + + const ids = upstreamNodeIds.concat( + filterAllUpstreamNodeIds(edges, upstreamNodeIds), + ); + + ids.forEach((x) => { + if (pre.every((y) => y !== x)) { + pre.push(x); + } + }); + + return pre; + }, []); +} + +export function buildOutputOptions( + outputs: Record = {}, + nodeId?: string, + parentLabel?: string | ReactNode, + icon?: ReactNode, +) { + return Object.keys(outputs).map((x) => ({ + label: x, + value: `${nodeId}@${x}`, + parentLabel, + icon, + type: outputs[x]?.type, + })); +} + +export function useBuildNodeOutputOptions(nodeId?: string) { + const nodes = useGraphStore((state) => state.nodes); + const edges = useGraphStore((state) => state.edges); + + const nodeOutputOptions = useMemo(() => { + if (!nodeId) { + return []; + } + const upstreamIds = filterAllUpstreamNodeIds(edges, [nodeId]); + + const nodeWithOutputList = nodes.filter( + (x) => + upstreamIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs), + ); + + return nodeWithOutputList + .filter((x) => x.id !== nodeId) + .map((x) => ({ + label: x.data.name, + value: x.id, + title: x.data.name, + options: buildOutputOptions( + x.data.form.outputs, + x.id, + x.data.name, + , + ), + })); + }, [edges, nodeId, nodes]); + + return nodeOutputOptions; +} + +// exclude nodes with branches +const ExcludedNodes = [ + Operator.Categorize, + Operator.Relevant, + Operator.Begin, + Operator.Note, +]; + +const StringList = [ + BeginQueryType.Line, + BeginQueryType.Paragraph, + BeginQueryType.Options, +]; + +function transferToVariableType(type: string) { + if (StringList.some((x) => x === type)) { + return VariableType.String; + } + return type; +} + +export function useBuildBeginVariableOptions() { + const inputs = useSelectBeginNodeDataInputs(); + + const options = useMemo(() => { + return [ + { + label: {t('flow.beginInput')}, + title: t('flow.beginInput'), + options: inputs.map((x) => ({ + label: x.name, + parentLabel: {t('flow.beginInput')}, + icon: , + value: `begin@${x.key}`, + type: transferToVariableType(x.type), + })), + }, + ]; + }, [inputs]); + + return options; +} + +export const useBuildVariableOptions = (nodeId?: string, parentId?: string) => { + const nodeOutputOptions = useBuildNodeOutputOptions(nodeId); + const parentNodeOutputOptions = useBuildNodeOutputOptions(parentId); + const beginOptions = useBuildBeginVariableOptions(); + + const options = useMemo(() => { + return [...beginOptions, ...nodeOutputOptions, ...parentNodeOutputOptions]; + }, [beginOptions, nodeOutputOptions, parentNodeOutputOptions]); + + return options; +}; + +export function useBuildQueryVariableOptions(n?: RAGFlowNodeType) { + const { data } = useFetchAgent(); + const node = useContext(AgentFormContext) || n; + const options = useBuildVariableOptions(node?.id, node?.parentId); + const nextOptions = useMemo(() => { + const globals = data?.dsl?.globals ?? {}; + const globalOptions = Object.entries(globals).map(([key, value]) => ({ + label: key, + value: key, + icon: , + parentLabel: {t('flow.beginInput')}, + type: Array.isArray(value) + ? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '' : ''}` + : typeof value, + })); + return [ + { ...options[0], options: [...options[0]?.options, ...globalOptions] }, + ...options.slice(1), + ]; + }, [data.dsl?.globals, options]); + + return nextOptions; +} + +export function useBuildComponentIdOptions(nodeId?: string, parentId?: string) { + const nodes = useGraphStore((state) => state.nodes); + + // Limit the nodes inside iteration to only reference peer nodes with the same parentId and other external nodes other than their parent nodes + const filterChildNodesToSameParentOrExternal = useCallback( + (node: RAGFlowNodeType) => { + // Node inside iteration + if (parentId) { + return ( + (node.parentId === parentId || node.parentId === undefined) && + node.id !== parentId + ); + } + + return node.parentId === undefined; // The outermost node + }, + [parentId], + ); + + const componentIdOptions = useMemo(() => { + return nodes + .filter( + (x) => + x.id !== nodeId && + !ExcludedNodes.some((y) => y === x.data.label) && + filterChildNodesToSameParentOrExternal(x), + ) + .map((x) => ({ label: x.data.name, value: x.id })); + }, [nodes, nodeId, filterChildNodesToSameParentOrExternal]); + + return [ + { + label: Component Output, + title: 'Component Output', + options: componentIdOptions, + }, + ]; +} + +export function useBuildComponentIdAndBeginOptions( + nodeId?: string, + parentId?: string, +) { + const componentIdOptions = useBuildComponentIdOptions(nodeId, parentId); + const beginOptions = useBuildBeginVariableOptions(); + + return [...beginOptions, ...componentIdOptions]; +} + +export const useGetComponentLabelByValue = (nodeId: string) => { + const options = useBuildComponentIdAndBeginOptions(nodeId); + + const flattenOptions = useMemo(() => { + return options.reduce((pre, cur) => { + return [...pre, ...cur.options]; + }, []); + }, [options]); + + const getLabel = useCallback( + (val?: string) => { + return flattenOptions.find((x) => x.value === val)?.label; + }, + [flattenOptions], + ); + return getLabel; +}; + +export function useGetVariableLabelByValue(nodeId: string) { + const { getNode } = useGraphStore((state) => state); + const nextOptions = useBuildQueryVariableOptions(getNode(nodeId)); + + const flattenOptions = useMemo(() => { + return nextOptions.reduce((pre, cur) => { + return [...pre, ...cur.options]; + }, []); + }, [nextOptions]); + + const getLabel = useCallback( + (val?: string) => { + return flattenOptions.find((x) => x.value === val)?.label; + }, + [flattenOptions], + ); + return getLabel; +} diff --git a/web/src/pages/data-flow/hooks/use-iteration.ts b/web/src/pages/data-flow/hooks/use-iteration.ts new file mode 100644 index 000000000..e69de29bb diff --git a/web/src/pages/data-flow/hooks/use-move-note.ts b/web/src/pages/data-flow/hooks/use-move-note.ts new file mode 100644 index 000000000..2c1d1b072 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-move-note.ts @@ -0,0 +1,35 @@ +import { useMouse } from 'ahooks'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +export function useMoveNote() { + const ref = useRef(null); + const mouse = useMouse(); + const [imgVisible, setImgVisible] = useState(false); + + const toggleVisible = useCallback((visible: boolean) => { + setImgVisible(visible); + }, []); + + const showImage = useCallback(() => { + toggleVisible(true); + }, [toggleVisible]); + + const hideImage = useCallback(() => { + toggleVisible(false); + }, [toggleVisible]); + + useEffect(() => { + if (ref.current) { + ref.current.style.top = `${mouse.clientY - 70}px`; + ref.current.style.left = `${mouse.clientX + 10}px`; + } + }, [mouse.clientX, mouse.clientY]); + + return { + ref, + showImage, + hideImage, + mouse, + imgVisible, + }; +} diff --git a/web/src/pages/data-flow/hooks/use-open-document.ts b/web/src/pages/data-flow/hooks/use-open-document.ts new file mode 100644 index 000000000..384529c15 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-open-document.ts @@ -0,0 +1,12 @@ +import { useCallback } from 'react'; + +export function useOpenDocument() { + const openDocument = useCallback(() => { + window.open( + 'https://ragflow.io/docs/dev/category/agent-components', + '_blank', + ); + }, []); + + return openDocument; +} diff --git a/web/src/pages/data-flow/hooks/use-save-graph.ts b/web/src/pages/data-flow/hooks/use-save-graph.ts new file mode 100644 index 000000000..8ce4dc15e --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-save-graph.ts @@ -0,0 +1,89 @@ +import { + useFetchAgent, + useResetAgent, + useSetAgent, +} from '@/hooks/use-agent-request'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { formatDate } from '@/utils/date'; +import { useDebounceEffect } from 'ahooks'; +import { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'umi'; +import useGraphStore from '../store'; +import { useBuildDslData } from './use-build-dsl'; + +export const useSaveGraph = (showMessage: boolean = true) => { + const { data } = useFetchAgent(); + const { setAgent, loading } = useSetAgent(showMessage); + const { id } = useParams(); + const { buildDslData } = useBuildDslData(); + + const saveGraph = useCallback( + async (currentNodes?: RAGFlowNodeType[]) => { + return setAgent({ + id, + title: data.title, + dsl: buildDslData(currentNodes), + }); + }, + [setAgent, data, id, buildDslData], + ); + + return { saveGraph, loading }; +}; + +export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { + const { saveGraph, loading } = useSaveGraph(); + const { resetAgent } = useResetAgent(); + + const handleRun = useCallback( + async (nextNodes?: RAGFlowNodeType[]) => { + const saveRet = await saveGraph(nextNodes); + if (saveRet?.code === 0) { + // Call the reset api before opening the run drawer each time + const resetRet = await resetAgent(); + // After resetting, all previous messages will be cleared. + if (resetRet?.code === 0) { + show(); + } + } + }, + [saveGraph, resetAgent, show], + ); + + return { handleRun, loading }; +}; + +export const useWatchAgentChange = (chatDrawerVisible: boolean) => { + const [time, setTime] = useState(); + const nodes = useGraphStore((state) => state.nodes); + const edges = useGraphStore((state) => state.edges); + const { saveGraph } = useSaveGraph(false); + const { data: flowDetail } = useFetchAgent(); + + const setSaveTime = useCallback((updateTime: number) => { + setTime(formatDate(updateTime)); + }, []); + + useEffect(() => { + setSaveTime(flowDetail?.update_time); + }, [flowDetail, setSaveTime]); + + const saveAgent = useCallback(async () => { + if (!chatDrawerVisible) { + const ret = await saveGraph(); + setSaveTime(ret.data.update_time); + } + }, [chatDrawerVisible, saveGraph, setSaveTime]); + + useDebounceEffect( + () => { + saveAgent(); + }, + [nodes, edges], + { + wait: 1000 * 20, + }, + ); + + return time; +}; diff --git a/web/src/pages/data-flow/hooks/use-send-shared-message.ts b/web/src/pages/data-flow/hooks/use-send-shared-message.ts new file mode 100644 index 000000000..d8e2d61de --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-send-shared-message.ts @@ -0,0 +1,79 @@ +import { SharedFrom } from '@/constants/chat'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { IEventList } from '@/hooks/use-send-message'; +import { + buildRequestBody, + useSendAgentMessage, +} from '@/pages/agent/chat/use-send-agent-message'; +import trim from 'lodash/trim'; +import { useCallback, useState } from 'react'; +import { useSearchParams } from 'umi'; + +export const useSendButtonDisabled = (value: string) => { + return trim(value) === ''; +}; + +export const useGetSharedChatSearchParams = () => { + const [searchParams] = useSearchParams(); + const data_prefix = 'data_'; + const data = Object.fromEntries( + searchParams + .entries() + .filter(([key]) => key.startsWith(data_prefix)) + .map(([key, value]) => [key.replace(data_prefix, ''), value]), + ); + return { + from: searchParams.get('from') as SharedFrom, + sharedId: searchParams.get('shared_id'), + locale: searchParams.get('locale'), + data: data, + visibleAvatar: searchParams.get('visible_avatar') + ? searchParams.get('visible_avatar') !== '1' + : true, + }; +}; + +export const useSendNextSharedMessage = ( + addEventList: (data: IEventList, messageId: string) => void, + isTaskMode: boolean, +) => { + const { from, sharedId: conversationId } = useGetSharedChatSearchParams(); + const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`; + + const [params, setParams] = useState([]); + + const { + visible: parameterDialogVisible, + hideModal: hideParameterDialog, + showModal: showParameterDialog, + } = useSetModalState(); + + const ret = useSendAgentMessage(url, addEventList, params, true); + + const ok = useCallback( + (params: any[]) => { + if (isTaskMode) { + const msgBody = buildRequestBody(''); + + ret.sendMessage({ + message: msgBody, + beginInputs: params, + }); + } else { + setParams(params); + } + + hideParameterDialog(); + }, + [hideParameterDialog, isTaskMode, ret], + ); + + return { + ...ret, + hasError: false, + parameterDialogVisible, + hideParameterDialog, + showParameterDialog, + ok, + }; +}; diff --git a/web/src/pages/data-flow/hooks/use-set-graph.ts b/web/src/pages/data-flow/hooks/use-set-graph.ts new file mode 100644 index 000000000..6dd68a330 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-set-graph.ts @@ -0,0 +1,17 @@ +import { IGraph } from '@/interfaces/database/flow'; +import { useCallback } from 'react'; +import useGraphStore from '../store'; + +export const useSetGraphInfo = () => { + const { setEdges, setNodes } = useGraphStore((state) => state); + const setGraphInfo = useCallback( + ({ nodes = [], edges = [] }: IGraph) => { + if (nodes.length || edges.length) { + setNodes(nodes); + setEdges(edges); + } + }, + [setEdges, setNodes], + ); + return setGraphInfo; +}; diff --git a/web/src/pages/data-flow/hooks/use-show-dialog.ts b/web/src/pages/data-flow/hooks/use-show-dialog.ts new file mode 100644 index 000000000..6178e3fbc --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-show-dialog.ts @@ -0,0 +1,91 @@ +import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog'; +import { SharedFrom } from '@/constants/chat'; +import { useShowDeleteConfirm } from '@/hooks/common-hooks'; +import { + useCreateSystemToken, + useFetchSystemTokenList, + useRemoveSystemToken, +} from '@/hooks/user-setting-hooks'; +import { IStats } from '@/interfaces/database/chat'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +export const useOperateApiKey = (idKey: string, dialogId?: string) => { + const { removeToken } = useRemoveSystemToken(); + const { createToken, loading: creatingLoading } = useCreateSystemToken(); + const { data: tokenList, loading: listLoading } = useFetchSystemTokenList(); + + const showDeleteConfirm = useShowDeleteConfirm(); + + const onRemoveToken = (token: string) => { + showDeleteConfirm({ + onOk: () => removeToken(token), + }); + }; + + const onCreateToken = useCallback(() => { + createToken({ [idKey]: dialogId }); + }, [createToken, idKey, dialogId]); + + return { + removeToken: onRemoveToken, + createToken: onCreateToken, + tokenList, + creatingLoading, + listLoading, + }; +}; + +type ChartStatsType = { + [k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>; +}; + +export const useSelectChartStatsList = (): ChartStatsType => { + const queryClient = useQueryClient(); + const data = queryClient.getQueriesData({ queryKey: ['fetchStats'] }); + const stats: IStats = (data.length > 0 ? data[0][1] : {}) as IStats; + + return Object.keys(stats).reduce((pre, cur) => { + const item = stats[cur as keyof IStats]; + if (item.length > 0) { + pre[cur as keyof IStats] = item.map((x) => ({ + xAxis: x[0] as string, + yAxis: x[1] as number, + })); + } + return pre; + }, {} as ChartStatsType); +}; + +const getUrlWithToken = (token: string, from: string = 'chat') => { + const { protocol, host } = window.location; + return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`; +}; + +export const usePreviewChat = (idKey: string) => { + const { handleOperate } = useFetchTokenListBeforeOtherStep(); + + const open = useCallback( + (t: string) => { + window.open( + getUrlWithToken( + t, + idKey === 'canvasId' ? SharedFrom.Agent : SharedFrom.Chat, + ), + '_blank', + ); + }, + [idKey], + ); + + const handlePreview = useCallback(async () => { + const token = await handleOperate(); + if (token) { + open(token); + } + }, [handleOperate, open]); + + return { + handlePreview, + }; +}; diff --git a/web/src/pages/data-flow/hooks/use-show-drawer.tsx b/web/src/pages/data-flow/hooks/use-show-drawer.tsx new file mode 100644 index 000000000..6789e86ba --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-show-drawer.tsx @@ -0,0 +1,186 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import { NodeMouseHandler } from '@xyflow/react'; +import get from 'lodash/get'; +import React, { useCallback, useEffect } from 'react'; +import { Operator } from '../constant'; +import useGraphStore from '../store'; +import { useCacheChatLog } from './use-cache-chat-log'; +import { useGetBeginNodeDataInputs } from './use-get-begin-query'; +import { useSaveGraph } from './use-save-graph'; + +export const useShowFormDrawer = () => { + const { + clickedNodeId: clickNodeId, + setClickedNodeId, + getNode, + setClickedToolId, + } = useGraphStore((state) => state); + const { + visible: formDrawerVisible, + hideModal: hideFormDrawer, + showModal: showFormDrawer, + } = useSetModalState(); + + const handleShow = useCallback( + (e: React.MouseEvent, nodeId: string) => { + const tool = get(e.target, 'dataset.tool'); + // TODO: Operator type judgment should be used + if (nodeId.startsWith(Operator.Tool) && !tool) { + return; + } + setClickedNodeId(nodeId); + setClickedToolId(tool); + showFormDrawer(); + }, + [setClickedNodeId, setClickedToolId, showFormDrawer], + ); + + return { + formDrawerVisible, + hideFormDrawer, + showFormDrawer: handleShow, + clickedNode: getNode(clickNodeId), + }; +}; + +export const useShowSingleDebugDrawer = () => { + const { visible, showModal, hideModal } = useSetModalState(); + const { saveGraph } = useSaveGraph(); + + const showSingleDebugDrawer = useCallback(async () => { + const saveRet = await saveGraph(); + if (saveRet?.code === 0) { + showModal(); + } + }, [saveGraph, showModal]); + + return { + singleDebugDrawerVisible: visible, + hideSingleDebugDrawer: hideModal, + showSingleDebugDrawer, + }; +}; + +const ExcludedNodes = [Operator.Note]; + +export function useShowDrawer({ + drawerVisible, + hideDrawer, +}: { + drawerVisible: boolean; + hideDrawer(): void; +}) { + const { + visible: runVisible, + showModal: showRunModal, + hideModal: hideRunModal, + } = useSetModalState(); + const { + visible: chatVisible, + showModal: showChatModal, + hideModal: hideChatModal, + } = useSetModalState(); + const { + singleDebugDrawerVisible, + showSingleDebugDrawer, + hideSingleDebugDrawer, + } = useShowSingleDebugDrawer(); + const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } = + useShowFormDrawer(); + const inputs = useGetBeginNodeDataInputs(); + + useEffect(() => { + if (drawerVisible) { + if (inputs.length > 0) { + showRunModal(); + hideChatModal(); + } else { + showChatModal(); + hideRunModal(); + } + } + }, [ + hideChatModal, + hideRunModal, + showChatModal, + showRunModal, + drawerVisible, + inputs, + ]); + + const hideRunOrChatDrawer = useCallback(() => { + hideChatModal(); + hideRunModal(); + hideDrawer(); + }, [hideChatModal, hideDrawer, hideRunModal]); + + const onPaneClick = useCallback(() => { + hideFormDrawer(); + }, [hideFormDrawer]); + + const onNodeClick: NodeMouseHandler = useCallback( + (e, node) => { + if (!ExcludedNodes.some((x) => x === node.data.label)) { + hideSingleDebugDrawer(); + // hideRunOrChatDrawer(); + showFormDrawer(e, node.id); + } + // handle single debug icon click + if ( + get(e.target, 'dataset.play') === 'true' || + get(e.target, 'parentNode.dataset.play') === 'true' + ) { + showSingleDebugDrawer(); + } + }, + [hideSingleDebugDrawer, showFormDrawer, showSingleDebugDrawer], + ); + + return { + chatVisible, + runVisible, + onPaneClick, + singleDebugDrawerVisible, + showSingleDebugDrawer, + hideSingleDebugDrawer, + formDrawerVisible, + showFormDrawer, + clickedNode, + onNodeClick, + hideFormDrawer, + hideRunOrChatDrawer, + showChatModal, + }; +} + +export function useShowLogSheet({ + setCurrentMessageId, +}: Pick, 'setCurrentMessageId'>) { + const { visible, showModal, hideModal } = useSetModalState(); + + const handleShow = useCallback( + (messageId: string) => { + setCurrentMessageId(messageId); + showModal(); + }, + [setCurrentMessageId, showModal], + ); + + return { + logSheetVisible: visible, + hideLogSheet: hideModal, + showLogSheet: handleShow, + }; +} + +export function useHideFormSheetOnNodeDeletion({ + hideFormDrawer, +}: Pick, 'hideFormDrawer'>) { + const { nodes, clickedNodeId } = useGraphStore((state) => state); + + useEffect(() => { + if (!nodes.some((x) => x.id === clickedNodeId)) { + hideFormDrawer(); + } + }, [clickedNodeId, hideFormDrawer, nodes]); +} diff --git a/web/src/pages/data-flow/hooks/use-watch-form-change.ts b/web/src/pages/data-flow/hooks/use-watch-form-change.ts new file mode 100644 index 000000000..534c2e2a9 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-watch-form-change.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../store'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id) { + values = form?.getValues() || {}; + let nextValues: any = values; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/data-flow/index.tsx b/web/src/pages/data-flow/index.tsx new file mode 100644 index 000000000..4a1fc81ae --- /dev/null +++ b/web/src/pages/data-flow/index.tsx @@ -0,0 +1,187 @@ +import { PageHeader } from '@/components/page-header'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb'; +import { Button, ButtonLoading } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { ReactFlowProvider } from '@xyflow/react'; +import { + ChevronDown, + CirclePlay, + Download, + History, + LaptopMinimalCheck, + Settings, + Upload, +} from 'lucide-react'; +import { ComponentPropsWithoutRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import AgentCanvas from './canvas'; +import { DropdownProvider } from './canvas/context'; +import { useHandleExportOrImportJsonFile } from './hooks/use-export-json'; +import { useFetchDataOnMount } from './hooks/use-fetch-data'; +import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query'; +import { + useSaveGraph, + useSaveGraphBeforeOpeningDebugDrawer, + useWatchAgentChange, +} from './hooks/use-save-graph'; +import { SettingDialog } from './setting-dialog'; +import { UploadAgentDialog } from './upload-agent-dialog'; +import { useAgentHistoryManager } from './use-agent-history-manager'; +import { VersionDialog } from './version-dialog'; + +function AgentDropdownMenuItem({ + children, + ...props +}: ComponentPropsWithoutRef) { + return ( + + {children} + + ); +} + +export default function DataFlow() { + const { navigateToAgents } = useNavigatePage(); + const { + visible: chatDrawerVisible, + hideModal: hideChatDrawer, + showModal: showChatDrawer, + } = useSetModalState(); + const { t } = useTranslation(); + useAgentHistoryManager(); + const { + handleExportJson, + handleImportJson, + fileUploadVisible, + onFileUploadOk, + hideFileUploadModal, + } = useHandleExportOrImportJsonFile(); + const { saveGraph, loading } = useSaveGraph(); + const { flowDetail: agentDetail } = useFetchDataOnMount(); + const inputs = useGetBeginNodeDataInputs(); + const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); + const handleRunAgent = useCallback(() => { + if (inputs.length > 0) { + showChatDrawer(); + } else { + handleRun(); + } + }, [handleRun, inputs, showChatDrawer]); + const { + visible: versionDialogVisible, + hideModal: hideVersionDialog, + showModal: showVersionDialog, + } = useSetModalState(); + + const { + visible: settingDialogVisible, + hideModal: hideSettingDialog, + showModal: showSettingDialog, + } = useSetModalState(); + + const time = useWatchAgentChange(chatDrawerVisible); + + return ( +
    + +
    + + + + + Agent + + + + + {agentDetail.title} + + + +
    + {t('flow.autosaved')} {time} +
    +
    +
    + saveGraph()} + loading={loading} + > + {t('flow.save')} + + + + + + + + + + + + {t('flow.import')} + + + + + {t('flow.export')} + + + + + {t('flow.setting')} + + + +
    +
    + + + + + + {fileUploadVisible && ( + + )} + + {versionDialogVisible && ( + + + + )} + {settingDialogVisible && ( + + )} +
    + ); +} diff --git a/web/src/pages/data-flow/interface.ts b/web/src/pages/data-flow/interface.ts new file mode 100644 index 000000000..0a6cba2f0 --- /dev/null +++ b/web/src/pages/data-flow/interface.ts @@ -0,0 +1,43 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { FormInstance } from 'antd'; + +export interface IOperatorForm { + onValuesChange?(changedValues: any, values: any): void; + form?: FormInstance; + node?: RAGFlowNodeType; + nodeId?: string; +} + +export interface INextOperatorForm { + node?: RAGFlowNodeType; + nodeId?: string; +} + +export interface IGenerateParameter { + id?: string; + key: string; + component_id?: string; +} + +export interface IInvokeVariable extends IGenerateParameter { + value?: string; +} + +export type IPosition = { top: number; right: number; idx: number }; + +export interface BeginQuery { + key: string; + type: string; + value: string; + optional: boolean; + name: string; + options: (number | string | boolean)[]; +} + +export type IInputs = { + avatar: string; + title: string; + inputs: Record; + prologue: string; + mode: string; +}; diff --git a/web/src/pages/data-flow/operator-icon.tsx b/web/src/pages/data-flow/operator-icon.tsx new file mode 100644 index 000000000..b8ebc34ba --- /dev/null +++ b/web/src/pages/data-flow/operator-icon.tsx @@ -0,0 +1,87 @@ +import { ReactComponent as ArxivIcon } from '@/assets/svg/arxiv.svg'; +import { ReactComponent as BingIcon } from '@/assets/svg/bing.svg'; +import { ReactComponent as CrawlerIcon } from '@/assets/svg/crawler.svg'; +import { ReactComponent as DuckIcon } from '@/assets/svg/duck.svg'; +import { ReactComponent as GithubIcon } from '@/assets/svg/github.svg'; +import { ReactComponent as GoogleScholarIcon } from '@/assets/svg/google-scholar.svg'; +import { ReactComponent as GoogleIcon } from '@/assets/svg/google.svg'; +import { ReactComponent as PubMedIcon } from '@/assets/svg/pubmed.svg'; +import { ReactComponent as SearXNGIcon } from '@/assets/svg/searxng.svg'; +import { ReactComponent as TavilyIcon } from '@/assets/svg/tavily.svg'; +import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; +import { ReactComponent as WikipediaIcon } from '@/assets/svg/wikipedia.svg'; +import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; + +import { IconFont } from '@/components/icon-font'; +import { cn } from '@/lib/utils'; +import { HousePlus } from 'lucide-react'; +import { Operator } from './constant'; + +interface IProps { + name: Operator; + className?: string; +} + +export const OperatorIconMap = { + [Operator.Retrieval]: 'KR', + [Operator.Begin]: 'house-plus', + [Operator.Categorize]: 'a-QuestionClassification', + [Operator.Message]: 'reply', + [Operator.Iteration]: 'loop', + [Operator.Switch]: 'condition', + [Operator.Code]: 'code-set', + [Operator.Agent]: 'agent-ai', + [Operator.UserFillUp]: 'await', + [Operator.StringTransform]: 'a-textprocessing', + [Operator.Note]: 'notebook-pen', + [Operator.ExeSQL]: 'executesql-0', + [Operator.Invoke]: 'httprequest-0', + [Operator.Email]: 'sendemail-0', +}; + +export const SVGIconMap = { + [Operator.ArXiv]: ArxivIcon, + [Operator.GitHub]: GithubIcon, + [Operator.Bing]: BingIcon, + [Operator.DuckDuckGo]: DuckIcon, + [Operator.Google]: GoogleIcon, + [Operator.GoogleScholar]: GoogleScholarIcon, + [Operator.PubMed]: PubMedIcon, + [Operator.SearXNG]: SearXNGIcon, + [Operator.TavilyExtract]: TavilyIcon, + [Operator.TavilySearch]: TavilyIcon, + [Operator.Wikipedia]: WikipediaIcon, + [Operator.YahooFinance]: YahooFinanceIcon, + [Operator.WenCai]: WenCaiIcon, + [Operator.Crawler]: CrawlerIcon, +}; + +const Empty = () => { + return
    ; +}; + +const OperatorIcon = ({ name, className }: IProps) => { + const Icon = OperatorIconMap[name as keyof typeof OperatorIconMap] || Empty; + const SvgIcon = SVGIconMap[name as keyof typeof SVGIconMap] || Empty; + + if (name === Operator.Begin) { + return ( +
    + +
    + ); + } + + return typeof Icon === 'string' ? ( + + ) : ( + + ); +}; + +export default OperatorIcon; diff --git a/web/src/pages/data-flow/options.ts b/web/src/pages/data-flow/options.ts new file mode 100644 index 000000000..4ad5a6457 --- /dev/null +++ b/web/src/pages/data-flow/options.ts @@ -0,0 +1,2174 @@ +import { upperFirst } from 'lodash'; + +export const LanguageOptions = [ + { + value: 'af', + label: 'Afrikaans', + }, + { + value: 'pl', + label: 'Polski', + }, + { + value: 'ar', + label: 'العربية', + }, + { + value: 'ast', + label: 'Asturianu', + }, + { + value: 'az', + label: 'Azərbaycanca', + }, + { + value: 'bg', + label: 'Български', + }, + { + value: 'nan', + label: '閩南語 / Bân-lâm-gú', + }, + { + value: 'bn', + label: 'বাংলা', + }, + { + value: 'be', + label: 'Беларуская', + }, + { + value: 'ca', + label: 'Català', + }, + { + value: 'cs', + label: 'Čeština', + }, + { + value: 'cy', + label: 'Cymraeg', + }, + { + value: 'da', + label: 'Dansk', + }, + { + value: 'de', + label: 'Deutsch', + }, + { + value: 'fr', + label: 'Français', + }, + { + value: 'et', + label: 'Eesti', + }, + { + value: 'el', + label: 'Ελληνικά', + }, + { + value: 'en', + label: 'English', + }, + { + value: 'es', + label: 'Español', + }, + { + value: 'eo', + label: 'Esperanto', + }, + { + value: 'eu', + label: 'Euskara', + }, + { + value: 'fa', + label: 'فارسی', + }, + { + value: 'fr', + label: 'Français', + }, + { + value: 'gl', + label: 'Galego', + }, + { + value: 'ko', + label: '한국어', + }, + { + value: 'hy', + label: 'Հայերեն', + }, + { + value: 'hi', + label: 'हिन्दी', + }, + { + value: 'hr', + label: 'Hrvatski', + }, + { + value: 'id', + label: 'Bahasa Indonesia', + }, + { + value: 'it', + label: 'Italiano', + }, + { + value: 'he', + label: 'עברית', + }, + { + value: 'ka', + label: 'ქართული', + }, + { + value: 'lld', + label: 'Ladin', + }, + { + value: 'la', + label: 'Latina', + }, + { + value: 'lv', + label: 'Latviešu', + }, + { + value: 'lt', + label: 'Lietuvių', + }, + { + value: 'hu', + label: 'Magyar', + }, + { + value: 'mk', + label: 'Македонски', + }, + { + value: 'arz', + label: 'مصرى', + }, + { + value: 'ms', + label: 'Bahasa Melayu', + }, + { + value: 'min', + label: 'Bahaso Minangkabau', + }, + { + value: 'my', + label: 'မြန်မာဘာသာ', + }, + { + value: 'nl', + label: 'Nederlands', + }, + { + value: 'ja', + label: '日本語', + }, + { + value: 'no', + label: 'Norsk (bokmål)', + }, + { + value: 'nn', + label: 'Norsk (nynorsk)', + }, + { + value: 'ce', + label: 'Нохчийн', + }, + { + value: 'uz', + label: 'Oʻzbekcha / Ўзбекча', + }, + { + value: 'pt', + label: 'Português', + }, + { + value: 'kk', + label: 'Қазақша / Qazaqşa / قازاقشا', + }, + { + value: 'ro', + label: 'Română', + }, + { + value: 'ru', + label: 'Русский', + }, + { + value: 'ceb', + label: 'Sinugboanong Binisaya', + }, + { + value: 'sk', + label: 'Slovenčina', + }, + { + value: 'sl', + label: 'Slovenščina', + }, + { + value: 'sr', + label: 'Српски / Srpski', + }, + { + value: 'sh', + label: 'Srpskohrvatski / Српскохрватски', + }, + { + value: 'fi', + label: 'Suomi', + }, + { + value: 'sv', + label: 'Svenska', + }, + { + value: 'ta', + label: 'தமிழ்', + }, + { + value: 'tt', + label: 'Татарча / Tatarça', + }, + { + value: 'th', + label: 'ภาษาไทย', + }, + { + value: 'tg', + label: 'Тоҷикӣ', + }, + { + value: 'azb', + label: 'تۆرکجه', + }, + { + value: 'tr', + label: 'Türkçe', + }, + { + value: 'uk', + label: 'Українська', + }, + { + value: 'ur', + label: 'اردو', + }, + { + value: 'vi', + label: 'Tiếng Việt', + }, + { + value: 'war', + label: 'Winaray', + }, + { + value: 'zh', + label: '中文', + }, + { + value: 'yue', + label: '粵語', + }, +]; + +export const GoogleLanguageOptions = [ + { + language_code: 'af', + language_name: 'Afrikaans', + }, + { + language_code: 'ak', + language_name: 'Akan', + }, + { + language_code: 'sq', + language_name: 'Albanian', + }, + { + language_code: 'ws', + language_name: 'Samoa', + }, + { + language_code: 'am', + language_name: 'Amharic', + }, + { + language_code: 'ar', + language_name: 'Arabic', + }, + { + language_code: 'hy', + language_name: 'Armenian', + }, + { + language_code: 'az', + language_name: 'Azerbaijani', + }, + { + language_code: 'eu', + language_name: 'Basque', + }, + { + language_code: 'be', + language_name: 'Belarusian', + }, + { + language_code: 'bem', + language_name: 'Bemba', + }, + { + language_code: 'bn', + language_name: 'Bengali', + }, + { + language_code: 'bh', + language_name: 'Bihari', + }, + { + language_code: 'xx-bork', + language_name: 'Bork, bork, bork!', + }, + { + language_code: 'bs', + language_name: 'Bosnian', + }, + { + language_code: 'br', + language_name: 'Breton', + }, + { + language_code: 'bg', + language_name: 'Bulgarian', + }, + { + language_code: 'bt', + language_name: 'Bhutanese', + }, + { + language_code: 'km', + language_name: 'Cambodian', + }, + { + language_code: 'ca', + language_name: 'Catalan', + }, + { + language_code: 'chr', + language_name: 'Cherokee', + }, + { + language_code: 'ny', + language_name: 'Chichewa', + }, + { + language_code: 'zh-cn', + language_name: 'Chinese (Simplified)', + }, + { + language_code: 'zh-tw', + language_name: 'Chinese (Traditional)', + }, + { + language_code: 'co', + language_name: 'Corsican', + }, + { + language_code: 'hr', + language_name: 'Croatian', + }, + { + language_code: 'cs', + language_name: 'Czech', + }, + { + language_code: 'da', + language_name: 'Danish', + }, + { + language_code: 'nl', + language_name: 'Dutch', + }, + { + language_code: 'xx-elmer', + language_name: 'Elmer Fudd', + }, + { + language_code: 'en', + language_name: 'English', + }, + { + language_code: 'eo', + language_name: 'Esperanto', + }, + { + language_code: 'et', + language_name: 'Estonian', + }, + { + language_code: 'ee', + language_name: 'Ewe', + }, + { + language_code: 'fo', + language_name: 'Faroese', + }, + { + language_code: 'tl', + language_name: 'Filipino', + }, + { + language_code: 'fi', + language_name: 'Finnish', + }, + { + language_code: 'fr', + language_name: 'French', + }, + { + language_code: 'fy', + language_name: 'Frisian', + }, + { + language_code: 'gaa', + language_name: 'Ga', + }, + { + language_code: 'gl', + language_name: 'Galician', + }, + { + language_code: 'ka', + language_name: 'Georgian', + }, + { + language_code: 'de', + language_name: 'German', + }, + { + language_code: 'el', + language_name: 'Greek', + }, + { + language_code: 'kl', + language_name: 'Greenlandic', + }, + { + language_code: 'gn', + language_name: 'Guarani', + }, + { + language_code: 'gu', + language_name: 'Gujarati', + }, + { + language_code: 'xx-hacker', + language_name: 'Hacker', + }, + { + language_code: 'ht', + language_name: 'Haitian Creole', + }, + { + language_code: 'ha', + language_name: 'Hausa', + }, + { + language_code: 'haw', + language_name: 'Hawaiian', + }, + { + language_code: 'iw', + language_name: 'Hebrew', + }, + { + language_code: 'hi', + language_name: 'Hindi', + }, + { + language_code: 'hu', + language_name: 'Hungarian', + }, + { + language_code: 'is', + language_name: 'Icelandic', + }, + { + language_code: 'ig', + language_name: 'Igbo', + }, + { + language_code: 'id', + language_name: 'Indonesian', + }, + { + language_code: 'ia', + language_name: 'Interlingua', + }, + { + language_code: 'ga', + language_name: 'Irish', + }, + { + language_code: 'it', + language_name: 'Italian', + }, + { + language_code: 'ja', + language_name: 'Japanese', + }, + { + language_code: 'jw', + language_name: 'Javanese', + }, + { + language_code: 'kn', + language_name: 'Kannada', + }, + { + language_code: 'kk', + language_name: 'Kazakh', + }, + { + language_code: 'rw', + language_name: 'Kinyarwanda', + }, + { + language_code: 'rn', + language_name: 'Kirundi', + }, + { + language_code: 'xx-klingon', + language_name: 'Klingon', + }, + { + language_code: 'kg', + language_name: 'Kongo', + }, + { + language_code: 'ko', + language_name: 'Korean', + }, + { + language_code: 'kri', + language_name: 'Krio (Sierra Leone)', + }, + { + language_code: 'ku', + language_name: 'Kurdish', + }, + { + language_code: 'ckb', + language_name: 'Kurdish (Soranî)', + }, + { + language_code: 'ky', + language_name: 'Kyrgyz', + }, + { + language_code: 'lo', + language_name: 'Laothian', + }, + { + language_code: 'la', + language_name: 'Latin', + }, + { + language_code: 'lv', + language_name: 'Latvian', + }, + { + language_code: 'ln', + language_name: 'Lingala', + }, + { + language_code: 'lt', + language_name: 'Lithuanian', + }, + { + language_code: 'loz', + language_name: 'Lozi', + }, + { + language_code: 'lg', + language_name: 'Luganda', + }, + { + language_code: 'ach', + language_name: 'Luo', + }, + { + language_code: 'mk', + language_name: 'Macedonian', + }, + { + language_code: 'mg', + language_name: 'Malagasy', + }, + { + language_code: 'ms', + language_name: 'Malay', + }, + { + language_code: 'ml', + language_name: 'Malayalam', + }, + { + language_code: 'mt', + language_name: 'Maltese', + }, + { + language_code: 'mv', + language_name: 'Maldives', + }, + { + language_code: 'mi', + language_name: 'Maori', + }, + { + language_code: 'mr', + language_name: 'Marathi', + }, + { + language_code: 'mfe', + language_name: 'Mauritian Creole', + }, + { + language_code: 'mo', + language_name: 'Moldavian', + }, + { + language_code: 'mn', + language_name: 'Mongolian', + }, + { + language_code: 'sr-me', + language_name: 'Montenegrin', + }, + { + language_code: 'my', + language_name: 'Myanmar', + }, + { + language_code: 'ne', + language_name: 'Nepali', + }, + { + language_code: 'pcm', + language_name: 'Nigerian Pidgin', + }, + { + language_code: 'nso', + language_name: 'Northern Sotho', + }, + { + language_code: 'no', + language_name: 'Norwegian', + }, + { + language_code: 'nn', + language_name: 'Norwegian (Nynorsk)', + }, + { + language_code: 'oc', + language_name: 'Occitan', + }, + { + language_code: 'or', + language_name: 'Oriya', + }, + { + language_code: 'om', + language_name: 'Oromo', + }, + { + language_code: 'ps', + language_name: 'Pashto', + }, + { + language_code: 'fa', + language_name: 'Persian', + }, + { + language_code: 'xx-pirate', + language_name: 'Pirate', + }, + { + language_code: 'pl', + language_name: 'Polish', + }, + { + language_code: 'pt', + language_name: 'Portuguese', + }, + { + language_code: 'pt-br', + language_name: 'Portuguese (Brazil)', + }, + { + language_code: 'pt-pt', + language_name: 'Portuguese (Portugal)', + }, + { + language_code: 'pa', + language_name: 'Punjabi', + }, + { + language_code: 'qu', + language_name: 'Quechua', + }, + { + language_code: 'ro', + language_name: 'Romanian', + }, + { + language_code: 'rm', + language_name: 'Romansh', + }, + { + language_code: 'nyn', + language_name: 'Runyakitara', + }, + { + language_code: 'ru', + language_name: 'Russian', + }, + { + language_code: 'gd', + language_name: 'Scots Gaelic', + }, + { + language_code: 'sr', + language_name: 'Serbian', + }, + { + language_code: 'sh', + language_name: 'Serbo-Croatian', + }, + { + language_code: 'st', + language_name: 'Sesotho', + }, + { + language_code: 'tn', + language_name: 'Setswana', + }, + { + language_code: 'crs', + language_name: 'Seychellois Creole', + }, + { + language_code: 'sn', + language_name: 'Shona', + }, + { + language_code: 'sd', + language_name: 'Sindhi', + }, + { + language_code: 'si', + language_name: 'Sinhalese', + }, + { + language_code: 'sk', + language_name: 'Slovak', + }, + { + language_code: 'sl', + language_name: 'Slovenian', + }, + { + language_code: 'so', + language_name: 'Somali', + }, + { + language_code: 'es', + language_name: 'Spanish', + }, + { + language_code: 'es-419', + language_name: 'Spanish (Latin American)', + }, + { + language_code: 'su', + language_name: 'Sundanese', + }, + { + language_code: 'sw', + language_name: 'Swahili', + }, + { + language_code: 'sv', + language_name: 'Swedish', + }, + { + language_code: 'tg', + language_name: 'Tajik', + }, + { + language_code: 'ta', + language_name: 'Tamil', + }, + { + language_code: 'tt', + language_name: 'Tatar', + }, + { + language_code: 'te', + language_name: 'Telugu', + }, + { + language_code: 'th', + language_name: 'Thai', + }, + { + language_code: 'ti', + language_name: 'Tigrinya', + }, + { + language_code: 'to', + language_name: 'Tonga', + }, + { + language_code: 'lua', + language_name: 'Tshiluba', + }, + { + language_code: 'tum', + language_name: 'Tumbuka', + }, + { + language_code: 'tr', + language_name: 'Turkish', + }, + { + language_code: 'tk', + language_name: 'Turkmen', + }, + { + language_code: 'tw', + language_name: 'Twi', + }, + { + language_code: 'ug', + language_name: 'Uighur', + }, + { + language_code: 'uk', + language_name: 'Ukrainian', + }, + { + language_code: 'ur', + language_name: 'Urdu', + }, + { + language_code: 'uz', + language_name: 'Uzbek', + }, + { + language_code: 'vu', + language_name: 'Vanuatu', + }, + { + language_code: 'vi', + language_name: 'Vietnamese', + }, + { + language_code: 'cy', + language_name: 'Welsh', + }, + { + language_code: 'wo', + language_name: 'Wolof', + }, + { + language_code: 'xh', + language_name: 'Xhosa', + }, + { + language_code: 'yi', + language_name: 'Yiddish', + }, + { + language_code: 'yo', + language_name: 'Yoruba', + }, + { + language_code: 'zu', + language_name: 'Zulu', + }, +].map((x) => ({ label: x.language_name, value: x.language_code })); + +export const GoogleCountryOptions = [ + { + country_code: 'af', + country_name: 'Afghanistan', + }, + { + country_code: 'al', + country_name: 'Albania', + }, + { + country_code: 'dz', + country_name: 'Algeria', + }, + { + country_code: 'as', + country_name: 'American Samoa', + }, + { + country_code: 'ad', + country_name: 'Andorra', + }, + { + country_code: 'ao', + country_name: 'Angola', + }, + { + country_code: 'ai', + country_name: 'Anguilla', + }, + { + country_code: 'aq', + country_name: 'Antarctica', + }, + { + country_code: 'ag', + country_name: 'Antigua and Barbuda', + }, + { + country_code: 'ar', + country_name: 'Argentina', + }, + { + country_code: 'am', + country_name: 'Armenia', + }, + { + country_code: 'aw', + country_name: 'Aruba', + }, + { + country_code: 'au', + country_name: 'Australia', + }, + { + country_code: 'at', + country_name: 'Austria', + }, + { + country_code: 'az', + country_name: 'Azerbaijan', + }, + { + country_code: 'bs', + country_name: 'Bahamas', + }, + { + country_code: 'bh', + country_name: 'Bahrain', + }, + { + country_code: 'bd', + country_name: 'Bangladesh', + }, + { + country_code: 'bb', + country_name: 'Barbados', + }, + { + country_code: 'by', + country_name: 'Belarus', + }, + { + country_code: 'be', + country_name: 'Belgium', + }, + { + country_code: 'bz', + country_name: 'Belize', + }, + { + country_code: 'bj', + country_name: 'Benin', + }, + { + country_code: 'bm', + country_name: 'Bermuda', + }, + { + country_code: 'bt', + country_name: 'Bhutan', + }, + { + country_code: 'bo', + country_name: 'Bolivia', + }, + { + country_code: 'ba', + country_name: 'Bosnia and Herzegovina', + }, + { + country_code: 'bw', + country_name: 'Botswana', + }, + { + country_code: 'bv', + country_name: 'Bouvet Island', + }, + { + country_code: 'br', + country_name: 'Brazil', + }, + { + country_code: 'io', + country_name: 'British Indian Ocean Territory', + }, + { + country_code: 'bn', + country_name: 'Brunei Darussalam', + }, + { + country_code: 'bg', + country_name: 'Bulgaria', + }, + { + country_code: 'bf', + country_name: 'Burkina Faso', + }, + { + country_code: 'bi', + country_name: 'Burundi', + }, + { + country_code: 'kh', + country_name: 'Cambodia', + }, + { + country_code: 'cm', + country_name: 'Cameroon', + }, + { + country_code: 'ca', + country_name: 'Canada', + }, + { + country_code: 'cv', + country_name: 'Cape Verde', + }, + { + country_code: 'ky', + country_name: 'Cayman Islands', + }, + { + country_code: 'cf', + country_name: 'Central African Republic', + }, + { + country_code: 'td', + country_name: 'Chad', + }, + { + country_code: 'cl', + country_name: 'Chile', + }, + { + country_code: 'cn', + country_name: 'China', + }, + { + country_code: 'cx', + country_name: 'Christmas Island', + }, + { + country_code: 'cc', + country_name: 'Cocos (Keeling) Islands', + }, + { + country_code: 'co', + country_name: 'Colombia', + }, + { + country_code: 'km', + country_name: 'Comoros', + }, + { + country_code: 'cg', + country_name: 'Congo', + }, + { + country_code: 'cd', + country_name: 'Congo, the Democratic Republic of the', + }, + { + country_code: 'ck', + country_name: 'Cook Islands', + }, + { + country_code: 'cr', + country_name: 'Costa Rica', + }, + { + country_code: 'ci', + country_name: "Cote D'ivoire", + }, + { + country_code: 'hr', + country_name: 'Croatia', + }, + { + country_code: 'cu', + country_name: 'Cuba', + }, + { + country_code: 'cy', + country_name: 'Cyprus', + }, + { + country_code: 'cz', + country_name: 'Czech Republic', + }, + { + country_code: 'dk', + country_name: 'Denmark', + }, + { + country_code: 'dj', + country_name: 'Djibouti', + }, + { + country_code: 'dm', + country_name: 'Dominica', + }, + { + country_code: 'do', + country_name: 'Dominican Republic', + }, + { + country_code: 'ec', + country_name: 'Ecuador', + }, + { + country_code: 'eg', + country_name: 'Egypt', + }, + { + country_code: 'sv', + country_name: 'El Salvador', + }, + { + country_code: 'gq', + country_name: 'Equatorial Guinea', + }, + { + country_code: 'er', + country_name: 'Eritrea', + }, + { + country_code: 'ee', + country_name: 'Estonia', + }, + { + country_code: 'et', + country_name: 'Ethiopia', + }, + { + country_code: 'fk', + country_name: 'Falkland Islands (Malvinas)', + }, + { + country_code: 'fo', + country_name: 'Faroe Islands', + }, + { + country_code: 'fj', + country_name: 'Fiji', + }, + { + country_code: 'fi', + country_name: 'Finland', + }, + { + country_code: 'fr', + country_name: 'France', + }, + { + country_code: 'gf', + country_name: 'French Guiana', + }, + { + country_code: 'pf', + country_name: 'French Polynesia', + }, + { + country_code: 'tf', + country_name: 'French Southern Territories', + }, + { + country_code: 'ga', + country_name: 'Gabon', + }, + { + country_code: 'gm', + country_name: 'Gambia', + }, + { + country_code: 'ge', + country_name: 'Georgia', + }, + { + country_code: 'de', + country_name: 'Germany', + }, + { + country_code: 'gh', + country_name: 'Ghana', + }, + { + country_code: 'gi', + country_name: 'Gibraltar', + }, + { + country_code: 'gr', + country_name: 'Greece', + }, + { + country_code: 'gl', + country_name: 'Greenland', + }, + { + country_code: 'gd', + country_name: 'Grenada', + }, + { + country_code: 'gp', + country_name: 'Guadeloupe', + }, + { + country_code: 'gu', + country_name: 'Guam', + }, + { + country_code: 'gt', + country_name: 'Guatemala', + }, + { + country_code: 'gn', + country_name: 'Guinea', + }, + { + country_code: 'gw', + country_name: 'Guinea-Bissau', + }, + { + country_code: 'gy', + country_name: 'Guyana', + }, + { + country_code: 'ht', + country_name: 'Haiti', + }, + { + country_code: 'hm', + country_name: 'Heard Island and Mcdonald Islands', + }, + { + country_code: 'va', + country_name: 'Holy See (Vatican City State)', + }, + { + country_code: 'hn', + country_name: 'Honduras', + }, + { + country_code: 'hk', + country_name: 'Hong Kong', + }, + { + country_code: 'hu', + country_name: 'Hungary', + }, + { + country_code: 'is', + country_name: 'Iceland', + }, + { + country_code: 'in', + country_name: 'India', + }, + { + country_code: 'id', + country_name: 'Indonesia', + }, + { + country_code: 'ir', + country_name: 'Iran, Islamic Republic of', + }, + { + country_code: 'iq', + country_name: 'Iraq', + }, + { + country_code: 'ie', + country_name: 'Ireland', + }, + { + country_code: 'il', + country_name: 'Israel', + }, + { + country_code: 'it', + country_name: 'Italy', + }, + { + country_code: 'jm', + country_name: 'Jamaica', + }, + { + country_code: 'jp', + country_name: 'Japan', + }, + { + country_code: 'jo', + country_name: 'Jordan', + }, + { + country_code: 'kz', + country_name: 'Kazakhstan', + }, + { + country_code: 'ke', + country_name: 'Kenya', + }, + { + country_code: 'ki', + country_name: 'Kiribati', + }, + { + country_code: 'kp', + country_name: "Korea, Democratic People's Republic of", + }, + { + country_code: 'kr', + country_name: 'Korea, Republic of', + }, + { + country_code: 'kw', + country_name: 'Kuwait', + }, + { + country_code: 'kg', + country_name: 'Kyrgyzstan', + }, + { + country_code: 'la', + country_name: "Lao People's Democratic Republic", + }, + { + country_code: 'lv', + country_name: 'Latvia', + }, + { + country_code: 'lb', + country_name: 'Lebanon', + }, + { + country_code: 'ls', + country_name: 'Lesotho', + }, + { + country_code: 'lr', + country_name: 'Liberia', + }, + { + country_code: 'ly', + country_name: 'Libyan Arab Jamahiriya', + }, + { + country_code: 'li', + country_name: 'Liechtenstein', + }, + { + country_code: 'lt', + country_name: 'Lithuania', + }, + { + country_code: 'lu', + country_name: 'Luxembourg', + }, + { + country_code: 'mo', + country_name: 'Macao', + }, + { + country_code: 'mk', + country_name: 'Macedonia, the Former Yugosalv Republic of', + }, + { + country_code: 'mg', + country_name: 'Madagascar', + }, + { + country_code: 'mw', + country_name: 'Malawi', + }, + { + country_code: 'my', + country_name: 'Malaysia', + }, + { + country_code: 'mv', + country_name: 'Maldives', + }, + { + country_code: 'ml', + country_name: 'Mali', + }, + { + country_code: 'mt', + country_name: 'Malta', + }, + { + country_code: 'mh', + country_name: 'Marshall Islands', + }, + { + country_code: 'mq', + country_name: 'Martinique', + }, + { + country_code: 'mr', + country_name: 'Mauritania', + }, + { + country_code: 'mu', + country_name: 'Mauritius', + }, + { + country_code: 'yt', + country_name: 'Mayotte', + }, + { + country_code: 'mx', + country_name: 'Mexico', + }, + { + country_code: 'fm', + country_name: 'Micronesia, Federated States of', + }, + { + country_code: 'md', + country_name: 'Moldova, Republic of', + }, + { + country_code: 'mc', + country_name: 'Monaco', + }, + { + country_code: 'mn', + country_name: 'Mongolia', + }, + { + country_code: 'ms', + country_name: 'Montserrat', + }, + { + country_code: 'ma', + country_name: 'Morocco', + }, + { + country_code: 'mz', + country_name: 'Mozambique', + }, + { + country_code: 'mm', + country_name: 'Myanmar', + }, + { + country_code: 'na', + country_name: 'Namibia', + }, + { + country_code: 'nr', + country_name: 'Nauru', + }, + { + country_code: 'np', + country_name: 'Nepal', + }, + { + country_code: 'nl', + country_name: 'Netherlands', + }, + { + country_code: 'an', + country_name: 'Netherlands Antilles', + }, + { + country_code: 'nc', + country_name: 'New Caledonia', + }, + { + country_code: 'nz', + country_name: 'New Zealand', + }, + { + country_code: 'ni', + country_name: 'Nicaragua', + }, + { + country_code: 'ne', + country_name: 'Niger', + }, + { + country_code: 'ng', + country_name: 'Nigeria', + }, + { + country_code: 'nu', + country_name: 'Niue', + }, + { + country_code: 'nf', + country_name: 'Norfolk Island', + }, + { + country_code: 'mp', + country_name: 'Northern Mariana Islands', + }, + { + country_code: 'no', + country_name: 'Norway', + }, + { + country_code: 'om', + country_name: 'Oman', + }, + { + country_code: 'pk', + country_name: 'Pakistan', + }, + { + country_code: 'pw', + country_name: 'Palau', + }, + { + country_code: 'ps', + country_name: 'Palestinian Territory, Occupied', + }, + { + country_code: 'pa', + country_name: 'Panama', + }, + { + country_code: 'pg', + country_name: 'Papua New Guinea', + }, + { + country_code: 'py', + country_name: 'Paraguay', + }, + { + country_code: 'pe', + country_name: 'Peru', + }, + { + country_code: 'ph', + country_name: 'Philippines', + }, + { + country_code: 'pn', + country_name: 'Pitcairn', + }, + { + country_code: 'pl', + country_name: 'Poland', + }, + { + country_code: 'pt', + country_name: 'Portugal', + }, + { + country_code: 'pr', + country_name: 'Puerto Rico', + }, + { + country_code: 'qa', + country_name: 'Qatar', + }, + { + country_code: 're', + country_name: 'Reunion', + }, + { + country_code: 'ro', + country_name: 'Romania', + }, + { + country_code: 'ru', + country_name: 'Russian Federation', + }, + { + country_code: 'rw', + country_name: 'Rwanda', + }, + { + country_code: 'sh', + country_name: 'Saint Helena', + }, + { + country_code: 'kn', + country_name: 'Saint Kitts and Nevis', + }, + { + country_code: 'lc', + country_name: 'Saint Lucia', + }, + { + country_code: 'pm', + country_name: 'Saint Pierre and Miquelon', + }, + { + country_code: 'vc', + country_name: 'Saint Vincent and the Grenadines', + }, + { + country_code: 'ws', + country_name: 'Samoa', + }, + { + country_code: 'sm', + country_name: 'San Marino', + }, + { + country_code: 'st', + country_name: 'Sao Tome and Principe', + }, + { + country_code: 'sa', + country_name: 'Saudi Arabia', + }, + { + country_code: 'sn', + country_name: 'Senegal', + }, + { + country_code: 'rs', + country_name: 'Serbia and Montenegro', + }, + { + country_code: 'sc', + country_name: 'Seychelles', + }, + { + country_code: 'sl', + country_name: 'Sierra Leone', + }, + { + country_code: 'sg', + country_name: 'Singapore', + }, + { + country_code: 'sk', + country_name: 'Slovakia', + }, + { + country_code: 'si', + country_name: 'Slovenia', + }, + { + country_code: 'sb', + country_name: 'Solomon Islands', + }, + { + country_code: 'so', + country_name: 'Somalia', + }, + { + country_code: 'za', + country_name: 'South Africa', + }, + { + country_code: 'gs', + country_name: 'South Georgia and the South Sandwich Islands', + }, + { + country_code: 'es', + country_name: 'Spain', + }, + { + country_code: 'lk', + country_name: 'Sri Lanka', + }, + { + country_code: 'sd', + country_name: 'Sudan', + }, + { + country_code: 'sr', + country_name: 'Suriname', + }, + { + country_code: 'sj', + country_name: 'Svalbard and Jan Mayen', + }, + { + country_code: 'sz', + country_name: 'Swaziland', + }, + { + country_code: 'se', + country_name: 'Sweden', + }, + { + country_code: 'ch', + country_name: 'Switzerland', + }, + { + country_code: 'sy', + country_name: 'Syrian Arab Republic', + }, + { + country_code: 'tw', + country_name: 'Taiwan, Province of China', + }, + { + country_code: 'tj', + country_name: 'Tajikistan', + }, + { + country_code: 'tz', + country_name: 'Tanzania, United Republic of', + }, + { + country_code: 'th', + country_name: 'Thailand', + }, + { + country_code: 'tl', + country_name: 'Timor-Leste', + }, + { + country_code: 'tg', + country_name: 'Togo', + }, + { + country_code: 'tk', + country_name: 'Tokelau', + }, + { + country_code: 'to', + country_name: 'Tonga', + }, + { + country_code: 'tt', + country_name: 'Trinidad and Tobago', + }, + { + country_code: 'tn', + country_name: 'Tunisia', + }, + { + country_code: 'tr', + country_name: 'Turkiye', + }, + { + country_code: 'tm', + country_name: 'Turkmenistan', + }, + { + country_code: 'tc', + country_name: 'Turks and Caicos Islands', + }, + { + country_code: 'tv', + country_name: 'Tuvalu', + }, + { + country_code: 'ug', + country_name: 'Uganda', + }, + { + country_code: 'ua', + country_name: 'Ukraine', + }, + { + country_code: 'ae', + country_name: 'United Arab Emirates', + }, + { + country_code: 'uk', + country_name: 'United Kingdom', + }, + { + country_code: 'gb', + country_name: 'United Kingdom', + }, + { + country_code: 'us', + country_name: 'United States', + }, + { + country_code: 'um', + country_name: 'United States Minor Outlying Islands', + }, + { + country_code: 'uy', + country_name: 'Uruguay', + }, + { + country_code: 'uz', + country_name: 'Uzbekistan', + }, + { + country_code: 'vu', + country_name: 'Vanuatu', + }, + { + country_code: 've', + country_name: 'Venezuela', + }, + { + country_code: 'vn', + country_name: 'Viet Nam', + }, + { + country_code: 'vg', + country_name: 'Virgin Islands, British', + }, + { + country_code: 'vi', + country_name: 'Virgin Islands, U.S.', + }, + { + country_code: 'wf', + country_name: 'Wallis and Futuna', + }, + { + country_code: 'eh', + country_name: 'Western Sahara', + }, + { + country_code: 'ye', + country_name: 'Yemen', + }, + { + country_code: 'zm', + country_name: 'Zambia', + }, + { + country_code: 'zw', + country_name: 'Zimbabwe', + }, +].map((x) => ({ label: x.country_name, value: x.country_code })); + +export const BingCountryOptions = [ + { label: 'Argentina AR', value: 'AR' }, + { label: 'Australia AU', value: 'AU' }, + { label: 'Austria AT', value: 'AT' }, + { label: 'Belgium BE', value: 'BE' }, + { label: 'Brazil BR', value: 'BR' }, + { label: 'Canada CA', value: 'CA' }, + { label: 'Chile CL', value: 'CL' }, + { label: 'Denmark DK', value: 'DK' }, + { label: 'Finland FI', value: 'FI' }, + { label: 'France FR', value: 'FR' }, + { label: 'Germany DE', value: 'DE' }, + { label: 'Hong Kong SAR HK', value: 'HK' }, + { label: 'India IN', value: 'IN' }, + { label: 'Indonesia ID', value: 'ID' }, + { label: 'Italy IT', value: 'IT' }, + { label: 'Japan JP', value: 'JP' }, + { label: 'Korea KR', value: 'KR' }, + { label: 'Malaysia MY', value: 'MY' }, + { label: 'Mexico MX', value: 'MX' }, + { label: 'Netherlands NL', value: 'NL' }, + { label: 'New Zealand NZ', value: 'NZ' }, + { label: 'Norway NO', value: 'NO' }, + { label: "People's Republic of China CN", value: 'CN' }, + { label: 'Poland PL', value: 'PL' }, + { label: 'Portugal PT', value: 'PT' }, + { label: 'Republic of the Philippines PH', value: 'PH' }, + { label: 'Russia RU', value: 'RU' }, + { label: 'Saudi Arabia SA', value: 'SA' }, + { label: 'South Africa ZA', value: 'ZA' }, + { label: 'Spain ES', value: 'ES' }, + { label: 'Sweden SE', value: 'SE' }, + { label: 'Switzerland CH', value: 'CH' }, + { label: 'Taiwan TW', value: 'TW' }, + { label: 'Türkiye TR', value: 'TR' }, + { label: 'United Kingdom GB', value: 'GB' }, + { label: 'United States US', value: 'US' }, +]; + +export const BingLanguageOptions = [ + { label: 'Arabic ar', value: 'ar' }, + { label: 'Basque eu', value: 'eu' }, + { label: 'Bengali bn', value: 'bn' }, + { label: 'Bulgarian bg', value: 'bg' }, + { label: 'Catalan ca', value: 'ca' }, + { label: 'Chinese (Simplified) zh-hans', value: 'ns' }, + { label: 'Chinese (Traditional) zh-hant', value: 'nt' }, + { label: 'Croatian hr', value: 'hr' }, + { label: 'Czech cs', value: 'cs' }, + { label: 'Danish da', value: 'da' }, + { label: 'Dutch nl', value: 'nl' }, + { label: 'English en', value: 'en' }, + { label: 'English-United Kingdom en-gb', value: 'gb' }, + { label: 'Estonian et', value: 'et' }, + { label: 'Finnish fi', value: 'fi' }, + { label: 'French fr', value: 'fr' }, + { label: 'Galician gl', value: 'gl' }, + { label: 'German de', value: 'de' }, + { label: 'Gujarati gu', value: 'gu' }, + { label: 'Hebrew he', value: 'he' }, + { label: 'Hindi hi', value: 'hi' }, + { label: 'Hungarian hu', value: 'hu' }, + { label: 'Icelandic is', value: 'is' }, + { label: 'Italian it', value: 'it' }, + { label: 'Japanese jp', value: 'jp' }, + { label: 'Kannada kn', value: 'kn' }, + { label: 'Korean ko', value: 'ko' }, + { label: 'Latvian lv', value: 'lv' }, + { label: 'Lithuanian lt', value: 'lt' }, + { label: 'Malay ms', value: 'ms' }, + { label: 'Malayalam ml', value: 'ml' }, + { label: 'Marathi mr', value: 'mr' }, + { label: 'Norwegian (Bokmål) nb', value: 'nb' }, + { label: 'Polish pl', value: 'pl' }, + { label: 'Portuguese (Brazil) pt-br', value: 'br' }, + { label: 'Portuguese (Portugal) pt-pt', value: 'pt' }, + { label: 'Punjabi pa', value: 'pa' }, + { label: 'Romanian ro', value: 'ro' }, + { label: 'Russian ru', value: 'ru' }, + { label: 'Serbian (Cyrylic) sr', value: 'sr' }, + { label: 'Slovak sk', value: 'sk' }, + { label: 'Slovenian sl', value: 'sl' }, + { label: 'Spanish es', value: 'es' }, + { label: 'Swedish sv', value: 'sv' }, + { label: 'Tamil ta', value: 'ta' }, + { label: 'Telugu te', value: 'te' }, + { label: 'Thai th', value: 'th' }, + { label: 'Turkish tr', value: 'tr' }, + { label: 'Ukrainian uk', value: 'uk' }, + { label: 'Vietnamese vi', value: 'vi' }, +]; + +export const DeepLSourceLangOptions = [ + { label: 'Arabic [1]', value: 'AR' }, + { label: 'Bulgarian', value: 'BG' }, + { label: 'Czech', value: 'CS' }, + { label: 'Danish', value: 'DA' }, + { label: 'German', value: 'DE' }, + { label: 'Greek', value: 'EL' }, + { label: 'English', value: 'EN' }, + { label: 'Spanish', value: 'ES' }, + { label: 'Estonian', value: 'ET' }, + { label: 'Finnish', value: 'FI' }, + { label: 'French', value: 'FR' }, + { label: 'Hungarian', value: 'HU' }, + { label: 'Indonesian', value: 'ID' }, + { label: 'Italian', value: 'IT' }, + { label: 'Japanese', value: 'JA' }, + { label: 'Korean', value: 'KO' }, + { label: 'Lithuanian', value: 'LT' }, + { label: 'Latvian', value: 'LV' }, + { label: 'Norwegian Bokmål', value: 'NB' }, + { label: 'Dutch', value: 'NL' }, + { label: 'Polish', value: 'PL' }, + { label: 'Portuguese (all Portuguese varieties mixed)', value: 'PT' }, + { label: 'Romanian', value: 'RO' }, + { label: 'Russian', value: 'RU' }, + { label: 'Slovak', value: 'SK' }, + { label: 'Slovenian', value: 'SL' }, + { label: 'Swedish', value: 'SV' }, + { label: 'Turkish', value: 'TR' }, + { label: 'Ukrainian', value: 'UK' }, + { label: 'Chinese', value: 'ZH' }, +]; +export const DeepLTargetLangOptions = [ + { label: 'Arabic [1]', value: 'AR' }, + { label: 'Bulgarian', value: 'BG' }, + { label: 'Czech', value: 'CS' }, + { label: 'Danish', value: 'DA' }, + { label: 'German', value: 'DE' }, + { label: 'Greek', value: 'EL' }, + { label: 'English (British)', value: 'EN-GB' }, + { label: 'English (American)', value: 'EN-US' }, + { label: 'Spanish', value: 'ES' }, + { label: 'Estonian', value: 'ET' }, + { label: 'Finnish', value: 'FI' }, + { label: 'French', value: 'FR' }, + { label: 'Hungarian', value: 'HU' }, + { label: 'Indonesian', value: 'ID' }, + { label: 'Italian', value: 'IT' }, + { label: 'Japanese', value: 'JA' }, + { label: 'Korean', value: 'KO' }, + { label: 'Lithuanian', value: 'LT' }, + { label: 'Latvian', value: 'LV' }, + { label: 'Norwegian Bokmål', value: 'NB' }, + { label: 'Dutch', value: 'NL' }, + { label: 'Polish', value: 'PL' }, + { label: 'Portuguese (Brazilian)', value: 'PT-BR' }, + { + label: + 'Portuguese (all Portuguese varieties excluding Brazilian Portuguese)', + value: 'PT-PT', + }, + { label: 'Romanian', value: 'RO' }, + { label: 'Russian', value: 'RU' }, + { label: 'Slovak', value: 'SK' }, + { label: 'Slovenian', value: 'SL' }, + { label: 'Swedish', value: 'SV' }, + { label: 'Turkish', value: 'TR' }, + { label: 'Ukrainian', value: 'UK' }, + { label: 'Chinese (simplified)', value: 'ZH' }, +]; + +export const BaiduFanyiDomainOptions = [ + 'it', + 'finance', + 'machinery', + 'senimed', + 'novel', + 'academic', + 'aerospace', + 'wiki', + 'news', + 'law', + 'contract', +]; + +export const BaiduFanyiSourceLangOptions = [ + 'auto', + 'zh', + 'en', + 'yue', + 'wyw', + 'jp', + 'kor', + 'fra', + 'spa', + 'th', + 'ara', + 'ru', + 'pt', + 'de', + 'it', + 'el', + 'nl', + 'pl', + 'bul', + 'est', + 'dan', + 'fin', + 'cs', + 'rom', + 'slo', + 'swe', + 'hu', + 'cht', + 'vie', +]; + +export const QWeatherLangOptions = [ + 'zh', + 'zh-hant', + 'en', + 'de', + 'es', + 'fr', + 'it', + 'ja', + 'ko', + 'ru', + 'hi', + 'th', + 'ar', + 'pt', + 'bn', + 'ms', + 'nl', + 'el', + 'la', + 'sv', + 'id', + 'pl', + 'tr', + 'cs', + 'et', + 'vi', + 'fil', + 'fi', + 'he', + 'is', + 'nb', +]; + +export const QWeatherTypeOptions = ['weather', 'indices', 'airquality']; + +export const QWeatherUserTypeOptions = ['free', 'paid']; + +export const QWeatherTimePeriodOptions = [ + 'now', + '3d', + '7d', + '10d', + '15d', + '30d', +]; + +export const ExeSQLOptions = ['mysql', 'postgresql', 'mariadb', 'mssql'].map( + (x) => ({ + label: upperFirst(x), + value: x, + }), +); + +export const WenCaiQueryTypeOptions = [ + 'stock', + 'zhishu', + 'fund', + 'hkstock', + 'usstock', + 'threeboard', + 'conbond', + 'insurance', + 'futures', + 'lccp', + 'foreign_exchange', +]; + +export const Jin10TypeOptions = ['flash', 'calendar', 'symbols', 'news']; +export const Jin10FlashTypeOptions = new Array(5) + .fill(1) + .map((x, idx) => (idx + 1).toString()); +export const Jin10CalendarTypeOptions = ['cj', 'qh', 'hk', 'us']; +export const Jin10CalendarDatashapeOptions = ['data', 'event', 'holiday']; +export const Jin10SymbolsTypeOptions = ['GOODS', 'FOREX', 'FUTURE', 'CRYPTO']; +export const Jin10SymbolsDatatypeOptions = ['symbols', 'quotes']; +export const TuShareSrcOptions = [ + 'sina', + 'wallstreetcn', + '10jqka', + 'eastmoney', + 'yuncaijing', + 'fenghuang', + 'jinrongjie', +]; +export const CrawlerResultOptions = ['markdown', 'html', 'content']; diff --git a/web/src/pages/data-flow/run-sheet/index.tsx b/web/src/pages/data-flow/run-sheet/index.tsx new file mode 100644 index 000000000..cac62d008 --- /dev/null +++ b/web/src/pages/data-flow/run-sheet/index.tsx @@ -0,0 +1,69 @@ +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { IModalProps } from '@/interfaces/common'; +import { cn } from '@/lib/utils'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BeginId } from '../constant'; +import DebugContent from '../debug-content'; +import { useGetBeginNodeDataInputs } from '../hooks/use-get-begin-query'; +import { useSaveGraphBeforeOpeningDebugDrawer } from '../hooks/use-save-graph'; +import { BeginQuery } from '../interface'; +import useGraphStore from '../store'; +import { buildBeginQueryWithObject } from '../utils'; + +const RunSheet = ({ + hideModal, + showModal: showChatModal, +}: IModalProps) => { + const { t } = useTranslation(); + const { updateNodeForm, getNode } = useGraphStore((state) => state); + + const inputs = useGetBeginNodeDataInputs(); + + const { handleRun, loading } = useSaveGraphBeforeOpeningDebugDrawer( + showChatModal!, + ); + + const handleRunAgent = useCallback( + (nextValues: BeginQuery[]) => { + const beginNode = getNode(BeginId); + const inputs: Record = beginNode?.data.form.inputs; + + const nextInputs = buildBeginQueryWithObject(inputs, nextValues); + + const currentNodes = updateNodeForm(BeginId, nextInputs, ['inputs']); + handleRun(currentNodes); + hideModal?.(); + }, + [getNode, handleRun, hideModal, updateNodeForm], + ); + + const onOk = useCallback( + async (nextValues: any[]) => { + handleRunAgent(nextValues); + }, + [handleRunAgent], + ); + + return ( + + + + {t('flow.testRun')} + + + + + ); +}; + +export default RunSheet; diff --git a/web/src/pages/data-flow/setting-dialog/index.tsx b/web/src/pages/data-flow/setting-dialog/index.tsx new file mode 100644 index 000000000..6d0e1e976 --- /dev/null +++ b/web/src/pages/data-flow/setting-dialog/index.tsx @@ -0,0 +1,53 @@ +import { ButtonLoading } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { useSetAgentSetting } from '@/hooks/use-agent-request'; +import { IModalProps } from '@/interfaces/common'; +import { transformFile2Base64 } from '@/utils/file-util'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + AgentSettingId, + SettingForm, + SettingFormSchemaType, +} from './setting-form'; + +export function SettingDialog({ hideModal }: IModalProps) { + const { t } = useTranslation(); + const { setAgentSetting } = useSetAgentSetting(); + + const submit = useCallback( + async (values: SettingFormSchemaType) => { + const avatar = values.avatar; + const code = await setAgentSetting({ + ...values, + avatar: avatar.length > 0 ? await transformFile2Base64(avatar[0]) : '', + }); + if (code === 0) { + hideModal?.(); + } + }, + [hideModal, setAgentSetting], + ); + + return ( + + + + Are you absolutely sure? + + + + + {t('common.save')} + + + + + ); +} diff --git a/web/src/pages/data-flow/setting-dialog/setting-form.tsx b/web/src/pages/data-flow/setting-dialog/setting-form.tsx new file mode 100644 index 000000000..e39ad2416 --- /dev/null +++ b/web/src/pages/data-flow/setting-dialog/setting-form.tsx @@ -0,0 +1,158 @@ +import { z } from 'zod'; + +import { + FileUpload, + FileUploadDropzone, + FileUploadItem, + FileUploadItemDelete, + FileUploadItemMetadata, + FileUploadItemPreview, + FileUploadList, + FileUploadTrigger, +} from '@/components/file-upload'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormItem, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Textarea } from '@/components/ui/textarea'; +import { useTranslate } from '@/hooks/common-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { transformBase64ToFile } from '@/utils/file-util'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { CloudUpload, X } from 'lucide-react'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; + +const formSchema = z.object({ + title: z.string().min(1, {}), + avatar: z.array(z.custom()).optional().nullable(), + description: z.string().optional().nullable(), + permission: z.string(), +}); + +export type SettingFormSchemaType = z.infer; + +export const AgentSettingId = 'agentSettingId'; + +type SettingFormProps = { + submit: (values: SettingFormSchemaType) => void; +}; + +export function SettingForm({ submit }: SettingFormProps) { + const { t } = useTranslate('flow.settings'); + const { data } = useFetchAgent(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + permission: 'me', + }, + }); + + useEffect(() => { + form.reset({ + title: data?.title, + description: data?.description, + avatar: data.avatar ? [transformBase64ToFile(data.avatar)] : [], + permission: data?.permission, + }); + }, [data, form]); + + return ( +
    + + + + + + {(field) => ( + { + form.setError('avatar', { + message, + }); + }} + multiple + > + + + Drag and drop or + + + + to upload + + + {field.value?.map((file: File, index: number) => ( + + + + + + + + ))} + + + )} + + +