From cbf04ee470333d846ac3c3bc0371eb2690cea0a4 Mon Sep 17 00:00:00 2001 From: Kevin Hu Date: Thu, 9 Oct 2025 12:36:19 +0800 Subject: [PATCH] Feat: Use data pipeline to visualize the parsing configuration of the knowledge base (#10423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What problem does this PR solve? #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Signed-off-by: dependabot[bot] Signed-off-by: jinhai Signed-off-by: Jin Hai Co-authored-by: chanx <1243304602@qq.com> Co-authored-by: balibabu Co-authored-by: Lynn Co-authored-by: 纷繁下的无奈 Co-authored-by: huangzl Co-authored-by: writinwaters <93570324+writinwaters@users.noreply.github.com> Co-authored-by: Wilmer <33392318@qq.com> Co-authored-by: Adrian Weidig Co-authored-by: Zhichang Yu Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Yongteng Lei Co-authored-by: Liu An Co-authored-by: buua436 <66937541+buua436@users.noreply.github.com> Co-authored-by: BadwomanCraZY <511528396@qq.com> Co-authored-by: cucusenok <31804608+cucusenok@users.noreply.github.com> Co-authored-by: Russell Valentine Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Billy Bao Co-authored-by: Zhedong Cen Co-authored-by: TensorNull <129579691+TensorNull@users.noreply.github.com> Co-authored-by: TensorNull Co-authored-by: TeslaZY Co-authored-by: Ajay <160579663+aybanda@users.noreply.github.com> Co-authored-by: AB Co-authored-by: 天海蒼灆 Co-authored-by: He Wang Co-authored-by: Atsushi Hatakeyama Co-authored-by: Jin Hai Co-authored-by: Mohamed Mathari <155896313+melmathari@users.noreply.github.com> Co-authored-by: Mohamed Mathari Co-authored-by: Stephen Hu Co-authored-by: Shaun Zhang Co-authored-by: zhimeng123 <60221886+zhimeng123@users.noreply.github.com> Co-authored-by: mxc Co-authored-by: Dominik Novotný <50611433+SgtMarmite@users.noreply.github.com> Co-authored-by: EVGENY M <168018528+rjohny55@users.noreply.github.com> Co-authored-by: mcoder6425 Co-authored-by: lemsn Co-authored-by: lemsn Co-authored-by: Adrian Gora <47756404+adagora@users.noreply.github.com> Co-authored-by: Womsxd <45663319+Womsxd@users.noreply.github.com> Co-authored-by: FatMii <39074672+FatMii@users.noreply.github.com> --- admin/exceptions.py | 17 + agent/canvas.py | 20 +- agent/component/llm.py | 24 +- api/apps/canvas_app.py | 105 +- api/apps/dataflow_app.py | 353 -- api/apps/document_app.py | 51 +- api/apps/file_app.py | 9 - api/apps/kb_app.py | 390 ++- api/db/__init__.py | 11 + api/db/db_models.py | 69 +- api/db/services/canvas_service.py | 23 +- api/db/services/document_service.py | 86 +- api/db/services/file_service.py | 3 +- api/db/services/knowledgebase_service.py | 23 +- .../pipeline_operation_log_service.py | 263 ++ api/db/services/task_service.py | 86 +- api/utils/api_utils.py | 4 +- api/utils/base64_image.py | 55 +- api/utils/file_utils.py | 2 +- api/utils/health.py | 104 + conf/llm_factories.json | 2 +- deepdoc/parser/pdf_parser.py | 16 +- graphrag/general/index.py | 209 +- pyproject.toml | 1 + rag/app/email.py | 2 +- rag/flow/base.py | 6 +- rag/flow/chunker/chunker.py | 212 -- rag/flow/{chunker => extractor}/__init__.py | 0 rag/flow/extractor/extractor.py | 63 + rag/flow/extractor/schema.py | 38 + rag/flow/file.py | 12 +- rag/flow/hierarchical_merger/__init__.py | 15 + .../hierarchical_merger.py | 186 + rag/flow/hierarchical_merger/schema.py | 37 + rag/flow/parser/parser.py | 327 +- rag/flow/parser/schema.py | 3 +- rag/flow/pipeline.py | 124 +- rag/flow/splitter/__init__.py | 15 + rag/flow/{chunker => splitter}/schema.py | 7 +- rag/flow/splitter/splitter.py | 111 + rag/flow/tests/client.py | 2 +- .../tests/dsl_examples/general_pdf_all.json | 34 +- .../dsl_examples/hierarchical_merger.json | 84 + rag/flow/tokenizer/schema.py | 12 +- rag/flow/tokenizer/tokenizer.py | 39 +- rag/llm/embedding_model.py | 20 +- rag/nlp/__init__.py | 6 +- rag/nlp/search.py | 21 +- rag/prompts/generator.py | 215 +- rag/prompts/toc_detection.md | 29 + rag/prompts/toc_extraction.md | 53 + rag/prompts/toc_extraction_continue.md | 60 + rag/prompts/toc_index.md | 20 + rag/svr/task_executor.py | 411 ++- web/public/iconfont.js | 62 +- .../assets/svg/data-flow/data-icon-bri.svg | 15 + web/src/assets/svg/data-flow/data-icon.svg | 15 + .../assets/svg/data-flow/knowledgegraph.svg | 1 - .../svg/data-flow/processing-icon-bri.svg | 6 + .../assets/svg/data-flow/processing-icon.svg | 6 + web/src/assets/svg/data-flow/raptor.svg | 1 - .../svg/data-flow/total-files-icon-bri.svg | 6 + .../assets/svg/data-flow/total-files-icon.svg | 6 + .../components/chunk-method-dialog/index.tsx | 247 +- .../use-default-parser-values.ts | 26 +- web/src/components/confirm-delete-dialog.tsx | 13 +- .../components/cross-language-form-field.tsx | 8 +- .../components/data-pipeline-select/index.tsx | 120 + web/src/components/delimiter-form-field.tsx | 13 +- .../components/entity-types-form-field.tsx | 2 +- web/src/components/file-status-badge.tsx | 31 +- web/src/components/home-card.tsx | 20 +- web/src/components/icon-font.tsx | 18 + .../layout-recognize-form-field.tsx | 66 +- .../llm-setting-items/llm-form-field.tsx | 25 + web/src/components/llm-setting-items/next.tsx | 27 +- .../originui/select-with-search.tsx | 58 +- web/src/components/originui/timeline.tsx | 20 +- .../graph-rag-form-fields.tsx | 53 +- .../raptor-form-fields.tsx | 61 +- web/src/components/ragflow-form.tsx | 20 +- .../components/slider-input-form-field.tsx | 3 +- web/src/components/ui/dual-range-slider.tsx | 2 +- web/src/components/ui/modal/modal.tsx | 14 +- web/src/components/ui/radio.tsx | 2 +- web/src/components/ui/tooltip.tsx | 9 +- web/src/constants/agent.ts | 26 + web/src/constants/knowledge.ts | 9 + web/src/hooks/flow-hooks.ts | 67 +- web/src/hooks/logic-hooks/navigate-hooks.ts | 31 +- web/src/hooks/route-hook.ts | 1 + web/src/hooks/use-agent-request.ts | 128 +- web/src/hooks/use-chunk-request.ts | 5 +- web/src/hooks/use-dataflow-request.ts | 91 + web/src/hooks/use-document-request.ts | 3 + web/src/hooks/use-knowledge-request.ts | 41 +- web/src/interfaces/database/agent.ts | 11 + web/src/interfaces/database/document.ts | 4 + web/src/interfaces/database/knowledge.ts | 7 + web/src/interfaces/request/document.ts | 1 + web/src/layouts/next-header.tsx | 25 +- web/src/less/mixins.less | 2 +- web/src/locales/en.ts | 191 +- web/src/locales/zh.ts | 172 +- web/src/pages/agent/canvas/edge/index.tsx | 6 +- web/src/pages/agent/constant.tsx | 24 +- .../pages/agent/debug-content/uploader.tsx | 2 +- .../form/components/prompt-editor/index.tsx | 4 +- .../prompt-editor/variable-picker-plugin.tsx | 67 +- .../form/iteration-form/use-build-options.ts | 2 +- .../pages/agent/hooks/use-get-begin-query.tsx | 80 +- .../agent/log-sheet/workflow-timeline.tsx | 16 +- web/src/pages/agent/run-sheet/index.tsx | 2 +- web/src/pages/agents/agent-card.tsx | 18 +- web/src/pages/agents/create-agent-dialog.tsx | 2 +- web/src/pages/agents/create-agent-form.tsx | 13 +- .../pages/agents/hooks/use-create-agent.ts | 98 +- .../pages/agents/hooks/use-selelct-filters.ts | 23 + web/src/pages/agents/index.tsx | 23 +- web/src/pages/agents/name-form-field.tsx | 7 +- .../components/knowledge-chunk/index.tsx | 4 +- web/src/pages/data-flow/canvas/edge/index.tsx | 44 +- web/src/pages/data-flow/canvas/index.tsx | 75 +- .../data-flow/canvas/node/agent-node.tsx | 116 - .../data-flow/canvas/node/begin-node.tsx | 2 +- .../data-flow/canvas/node/categorize-node.tsx | 62 - .../data-flow/canvas/node/chunker-node.tsx | 49 - .../node/dropdown/next-step-dropdown.tsx | 112 +- .../data-flow/canvas/node/email-node.tsx | 80 - .../data-flow/canvas/node/extractor-node.tsx | 1 + .../data-flow/canvas/node/generate-node.tsx | 60 - .../pages/data-flow/canvas/node/handle.tsx | 13 +- .../canvas/node/hierarchical-merger-node.tsx | 1 + web/src/pages/data-flow/canvas/node/index.tsx | 22 +- .../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/parser-node.tsx | 49 +- .../data-flow/canvas/node/relevant-node.tsx | 73 - .../data-flow/canvas/node/retrieval-node.tsx | 84 - .../data-flow/canvas/node/rewrite-node.tsx | 60 - .../data-flow/canvas/node/splitter-node.tsx | 1 + .../data-flow/canvas/node/switch-node.tsx | 118 - .../data-flow/canvas/node/template-node.tsx | 78 - .../data-flow/canvas/node/tokenizer-node.tsx | 15 +- .../pages/data-flow/canvas/node/tool-node.tsx | 83 - .../pages/data-flow/canvas/node/toolbar.tsx | 27 +- .../use-build-categorize-handle-positions.ts | 48 - .../node/use-build-switch-handle-positions.ts | 59 - web/src/pages/data-flow/constant.tsx | 604 ++-- web/src/pages/data-flow/context.ts | 8 + .../data-flow/form-sheet/form-config-map.tsx | 91 +- web/src/pages/data-flow/form-sheet/next.tsx | 68 +- .../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 - .../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 - .../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 - .../data-flow/form/chunker-form/index.tsx | 140 - .../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 - .../next-dynamic-input-variable.tsx | 135 - .../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/email-form/index.tsx | 161 - .../data-flow/form/exesql-form/index.tsx | 167 - .../form/exesql-form/use-submit-form.ts | 31 - .../data-flow/form/extractor-form/index.tsx | 102 + .../form/extractor-form/use-switch-prompt.ts | 69 + .../form/hierarchical-merger-form/index.tsx | 188 ++ .../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 - .../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 - .../form/parser-form/common-form-fields.tsx | 85 + .../form/parser-form/email-form-fields.tsx | 30 + .../form/parser-form/image-form-fields.tsx | 57 + .../data-flow/form/parser-form/index.tsx | 259 +- .../data-flow/form/parser-form/interface.ts | 3 + .../form/parser-form/pdf-form-fields.tsx | 44 + .../parser-form/use-set-initial-language.ts | 29 + .../pages/data-flow/form/parser-form/utils.ts | 3 + .../form/parser-form/video-form-fields.tsx | 13 + .../data-flow/form/relevant-form/hooks.ts | 41 - .../data-flow/form/relevant-form/index.tsx | 49 - .../data-flow/form/retrieval-form/next.tsx | 130 - .../form/retrieval-form/use-values.ts | 25 - .../form/rewrite-question-form/index.tsx | 68 - .../data-flow/form/splitter-form/index.tsx | 102 + .../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 - .../data-flow/form/tokenizer-form/index.tsx | 156 +- .../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 - web/src/pages/data-flow/hooks/use-add-node.ts | 254 +- .../data-flow/hooks/use-before-delete.tsx | 39 +- .../data-flow/hooks/use-build-options.tsx | 19 + .../data-flow/hooks/use-cancel-dataflow.ts | 21 + .../data-flow/hooks/use-change-node-name.ts | 14 +- .../pages/data-flow/hooks/use-chat-logic.ts | 60 - .../data-flow/hooks/use-download-output.ts | 38 + .../pages/data-flow/hooks/use-export-json.ts | 54 - .../pages/data-flow/hooks/use-fetch-log.ts | 56 + .../data-flow/hooks/use-find-mcp-by-id.ts | 12 - .../data-flow/hooks/use-get-begin-query.tsx | 317 -- .../pages/data-flow/hooks/use-iteration.ts | 0 .../pages/data-flow/hooks/use-run-dataflow.ts | 59 + .../pages/data-flow/hooks/use-save-graph.ts | 20 +- .../pages/data-flow/hooks/use-show-drawer.tsx | 28 +- .../data-flow/hooks/use-watch-form-change.ts | 9 +- web/src/pages/data-flow/index.tsx | 128 +- .../data-flow/log-sheet/dataflow-timeline.tsx | 137 + web/src/pages/data-flow/log-sheet/index.tsx | 111 + web/src/pages/data-flow/operator-icon.tsx | 28 +- web/src/pages/data-flow/options.ts | 2178 ------------ web/src/pages/data-flow/run-sheet/index.tsx | 54 +- .../pages/data-flow/run-sheet/uploader.tsx | 57 + web/src/pages/data-flow/store.ts | 110 +- .../data-flow/upload-agent-dialog/index.tsx | 36 - .../upload-agent-dialog/upload-agent-form.tsx | 89 - web/src/pages/data-flow/utils.ts | 361 +- web/src/pages/data-flow/utils/chat.ts | 21 - web/src/pages/dataflow-result/chunker.tsx | 31 +- .../chunk-result-bar/checkbox-sets.tsx | 33 +- .../components/chunk-result-bar/index.tsx | 74 +- .../components/document-preview/hooks.ts | 11 +- .../components/parse-editer/hook.ts | 30 + .../components/parse-editer/index.tsx | 78 +- .../components/parse-editer/interface.ts | 65 + .../components/parse-editer/json-parser.tsx | 139 + .../components/parse-editer/object-parser.tsx | 96 + .../components/rerun-button/index.tsx | 32 +- .../components/time-line/index.tsx | 91 +- web/src/pages/dataflow-result/constant.ts | 21 + web/src/pages/dataflow-result/hooks.ts | 284 +- web/src/pages/dataflow-result/index.less | 13 +- web/src/pages/dataflow-result/index.tsx | 205 +- web/src/pages/dataflow-result/interface.ts | 82 + web/src/pages/dataflow-result/parser.tsx | 211 +- .../dataset-overview/dataset-common.ts | 11 +- .../pages/dataset/dataset-overview/hook.ts | 96 + .../pages/dataset/dataset-overview/index.tsx | 291 +- .../dataset/dataset-overview/interface.ts | 62 + .../dataset-overview/overview-table.tsx | 351 +- .../dataset-setting/chunk-method-form.tsx | 31 - .../components/link-data-pipeline.tsx | 191 ++ .../components/link-data-pipline-modal.tsx | 163 + .../configuration-form-container.tsx | 7 +- .../configuration/common-item.tsx | 252 +- .../dataset/dataset-setting/form-schema.ts | 18 + .../dataset/dataset-setting/general-form.tsx | 15 +- .../pages/dataset/dataset-setting/hooks.ts | 18 +- .../pages/dataset/dataset-setting/index.tsx | 123 +- .../pages/dataset/dataset-setting/naive.tsx | 33 - .../dataset/dataset-setting/saving-button.tsx | 2 +- web/src/pages/dataset/dataset/constant.ts | 14 +- .../pages/dataset/dataset/dataset-table.tsx | 14 + .../dataset/generate-button/generate.tsx | 339 ++ .../dataset/dataset/generate-button/hook.ts | 174 + web/src/pages/dataset/dataset/generate.tsx | 68 - web/src/pages/dataset/dataset/hooks.ts | 47 +- web/src/pages/dataset/dataset/index.tsx | 4 +- .../pages/dataset/dataset/parsing-card.tsx | 26 +- .../dataset/dataset/parsing-status-cell.tsx | 134 +- .../dataset/use-change-document-parser.ts | 1 + .../dataset/use-dataset-table-columns.tsx | 4 +- web/src/pages/dataset/process-log-modal.tsx | 155 + web/src/pages/dataset/sidebar/index.tsx | 28 +- .../datasets/dataset-creating-dialog.tsx | 92 +- web/src/pages/datasets/hooks.ts | 12 +- web/src/pages/datasets/process-log-modal.tsx | 116 - web/src/pages/datasets/use-select-owners.ts | 13 +- .../pages/flow/canvas/context-menu/index.less | 18 - .../pages/flow/canvas/context-menu/index.tsx | 107 - web/src/pages/flow/canvas/edge/index.less | 31 - web/src/pages/flow/canvas/edge/index.tsx | 108 - web/src/pages/flow/canvas/index.less | 10 - web/src/pages/flow/canvas/index.tsx | 237 -- web/src/pages/flow/canvas/node/begin-node.tsx | 72 - web/src/pages/flow/canvas/node/card.tsx | 57 - .../flow/canvas/node/categorize-handle.tsx | 40 - .../flow/canvas/node/categorize-node.tsx | 68 - web/src/pages/flow/canvas/node/dropdown.tsx | 58 - web/src/pages/flow/canvas/node/email-node.tsx | 78 - .../pages/flow/canvas/node/generate-node.tsx | 57 - .../pages/flow/canvas/node/handle-icon.tsx | 20 - web/src/pages/flow/canvas/node/hooks.ts | 104 - web/src/pages/flow/canvas/node/index.less | 285 -- web/src/pages/flow/canvas/node/index.tsx | 45 - .../pages/flow/canvas/node/invoke-node.tsx | 59 - .../pages/flow/canvas/node/iteration-node.tsx | 127 - .../pages/flow/canvas/node/keyword-node.tsx | 57 - web/src/pages/flow/canvas/node/logic-node.tsx | 45 - .../pages/flow/canvas/node/message-node.tsx | 65 - .../pages/flow/canvas/node/node-header.tsx | 73 - web/src/pages/flow/canvas/node/note-node.tsx | 92 - web/src/pages/flow/canvas/node/popover.tsx | 313 -- .../pages/flow/canvas/node/relevant-node.tsx | 70 - .../pages/flow/canvas/node/retrieval-node.tsx | 115 - .../pages/flow/canvas/node/rewrite-node.tsx | 57 - .../pages/flow/canvas/node/switch-node.tsx | 114 - .../pages/flow/canvas/node/template-node.tsx | 75 - web/src/pages/flow/chat/box.tsx | 92 - web/src/pages/flow/chat/drawer.tsx | 25 - web/src/pages/flow/chat/hooks.ts | 145 - web/src/pages/flow/chat/index.less | 8 - web/src/pages/flow/constant.tsx | 3006 ----------------- web/src/pages/flow/context.ts | 6 - web/src/pages/flow/customer_service.json | 312 -- web/src/pages/flow/debug-content/index.less | 5 - web/src/pages/flow/debug-content/index.tsx | 250 -- .../pages/flow/debug-content/popover-form.tsx | 74 - web/src/pages/flow/flow-drawer/index.less | 21 - web/src/pages/flow/flow-drawer/index.tsx | 218 -- .../flow-drawer/single-debug-drawer/index.tsx | 81 - web/src/pages/flow/flow-id-modal/index.less | 3 - web/src/pages/flow/flow-id-modal/index.tsx | 36 - web/src/pages/flow/flow-setting/index.less | 0 web/src/pages/flow/flow-setting/index.tsx | 121 - web/src/pages/flow/flow-sider/index.less | 16 - web/src/pages/flow/flow-sider/index.tsx | 75 - web/src/pages/flow/flow-tooltip.tsx | 19 - web/src/pages/flow/form-hooks.ts | 77 - .../pages/flow/form/akshare-form/index.tsx | 21 - web/src/pages/flow/form/answer-form/index.tsx | 5 - web/src/pages/flow/form/arxiv-form/index.tsx | 36 - .../flow/form/baidu-fanyi-form/index.tsx | 71 - web/src/pages/flow/form/baidu-form/index.tsx | 21 - .../form/begin-form/begin-dynamic-options.tsx | 68 - web/src/pages/flow/form/begin-form/hooks.ts | 50 - web/src/pages/flow/form/begin-form/index.less | 24 - web/src/pages/flow/form/begin-form/index.tsx | 111 - .../flow/form/begin-form/paramater-modal.tsx | 124 - .../flow/form/begin-form/query-table.tsx | 92 - web/src/pages/flow/form/bing-form/index.tsx | 42 - .../categorize-form/dynamic-categorize.tsx | 225 -- .../pages/flow/form/categorize-form/hooks.ts | 45 - .../flow/form/categorize-form/index.less | 13 - .../pages/flow/form/categorize-form/index.tsx | 43 - .../form/code-form/dynamic-input-variable.tsx | 66 - .../code-form/dynamic-output-variable.tsx | 66 - web/src/pages/flow/form/code-form/index.less | 16 - web/src/pages/flow/form/code-form/index.tsx | 73 - .../components/dynamic-input-variable.tsx | 133 - web/src/pages/flow/form/components/index.less | 22 - .../flow/form/concentrator-form/index.tsx | 17 - .../pages/flow/form/crawler-form/index.tsx | 38 - web/src/pages/flow/form/deepl-form/index.tsx | 36 - .../pages/flow/form/duckduckgo-form/index.tsx | 38 - web/src/pages/flow/form/email-form/index.tsx | 53 - web/src/pages/flow/form/exesql-form/index.tsx | 88 - .../form/generate-form/dynamic-parameters.tsx | 101 - .../pages/flow/form/generate-form/hooks.ts | 70 - .../pages/flow/form/generate-form/index.less | 21 - .../pages/flow/form/generate-form/index.tsx | 72 - web/src/pages/flow/form/github-form/index.tsx | 21 - web/src/pages/flow/form/google-form/index.tsx | 34 - .../flow/form/google-scholar-form/index.tsx | 75 - .../form/invoke-form/dynamic-variables.tsx | 130 - web/src/pages/flow/form/invoke-form/hooks.ts | 97 - .../pages/flow/form/invoke-form/index.less | 44 - web/src/pages/flow/form/invoke-form/index.tsx | 87 - .../pages/flow/form/iteration-from/index.tsx | 94 - web/src/pages/flow/form/jin10-form/index.tsx | 145 - .../flow/form/keyword-extract-form/index.tsx | 32 - .../pages/flow/form/message-form/index.less | 16 - .../pages/flow/form/message-form/index.tsx | 87 - web/src/pages/flow/form/pubmed-form/index.tsx | 32 - .../pages/flow/form/qweather-form/index.tsx | 88 - .../pages/flow/form/relevant-form/hooks.ts | 52 - .../pages/flow/form/relevant-form/index.tsx | 49 - .../pages/flow/form/retrieval-form/index.tsx | 65 - .../flow/form/rewrite-question-form/index.tsx | 41 - .../pages/flow/form/switch-form/index.less | 21 - web/src/pages/flow/form/switch-form/index.tsx | 204 -- .../pages/flow/form/template-form/index.tsx | 24 - .../pages/flow/form/tushare-form/index.tsx | 83 - web/src/pages/flow/form/wencai-form/index.tsx | 36 - .../pages/flow/form/wikipedia-form/index.tsx | 28 - .../flow/form/yahoo-finance-form/index.tsx | 40 - web/src/pages/flow/header/index.less | 10 - web/src/pages/flow/header/index.tsx | 163 - web/src/pages/flow/headhunter_zh.json | 397 --- .../flow/history-version-modal/index.tsx | 184 - web/src/pages/flow/hooks.tsx | 572 ---- .../pages/flow/hooks/use-before-delete.tsx | 57 - web/src/pages/flow/hooks/use-build-dsl.ts | 29 - web/src/pages/flow/hooks/use-export-json.ts | 62 - web/src/pages/flow/hooks/use-fetch-data.ts | 19 - .../pages/flow/hooks/use-get-begin-query.tsx | 113 - web/src/pages/flow/hooks/use-iteration.ts | 0 web/src/pages/flow/hooks/use-open-document.ts | 12 - web/src/pages/flow/hooks/use-save-graph.ts | 89 - web/src/pages/flow/hooks/use-set-graph.ts | 17 - web/src/pages/flow/hooks/use-show-drawer.tsx | 153 - web/src/pages/flow/index.tsx | 45 - web/src/pages/flow/interface.ts | 30 - web/src/pages/flow/interpreter.json | 75 - .../pages/flow/json-upload-modal/index.less | 13 - .../pages/flow/json-upload-modal/index.tsx | 97 - .../pages/flow/list/agent-template-modal.tsx | 129 - .../pages/flow/list/create-agent-modal.tsx | 60 - web/src/pages/flow/list/flow-card/index.less | 84 - web/src/pages/flow/list/flow-card/index.tsx | 74 - web/src/pages/flow/list/graph-avatar.tsx | 16 - web/src/pages/flow/list/hooks.ts | 94 - web/src/pages/flow/list/index.less | 99 - web/src/pages/flow/list/index.tsx | 121 - web/src/pages/flow/mock.tsx | 183 - web/src/pages/flow/operator-icon/index.less | 6 - web/src/pages/flow/operator-icon/index.tsx | 24 - ...trieval_relevant_rewrite_and_generate.json | 160 - web/src/pages/flow/run-drawer/index.less | 5 - web/src/pages/flow/run-drawer/index.tsx | 62 - .../pages/flow/run-drawer/popover-form.tsx | 74 - web/src/pages/flow/store.ts | 455 --- web/src/pages/flow/utils.test.ts | 106 - web/src/pages/flow/utils.ts | 418 --- .../profile-setting/mcp/edit-mcp-dialog.tsx | 6 +- web/src/pages/profile-setting/mcp/index.tsx | 4 +- web/src/routes.ts | 10 +- web/src/services/agent-service.ts | 19 +- web/src/services/dataflow-service.ts | 37 + web/src/services/knowledge-service.ts | 64 + .../slider-input-form-field.stories.tsx | 171 + web/src/utils/api.ts | 22 +- web/src/utils/canvas-util.tsx | 75 + web/src/utils/common-util.ts | 44 +- web/src/utils/component-util.ts | 12 +- web/src/utils/dataset-util.ts | 24 - web/src/utils/date.ts | 18 + web/src/utils/form.ts | 4 +- web/src/utils/list-filter-util.ts | 29 + web/tailwind.config.js | 22 +- web/tailwind.css | 15 +- 490 files changed, 10630 insertions(+), 30688 deletions(-) create mode 100644 admin/exceptions.py delete mode 100644 api/apps/dataflow_app.py create mode 100644 api/db/services/pipeline_operation_log_service.py create mode 100644 api/utils/health.py delete mode 100644 rag/flow/chunker/chunker.py rename rag/flow/{chunker => extractor}/__init__.py (100%) create mode 100644 rag/flow/extractor/extractor.py create mode 100644 rag/flow/extractor/schema.py create mode 100644 rag/flow/hierarchical_merger/__init__.py create mode 100644 rag/flow/hierarchical_merger/hierarchical_merger.py create mode 100644 rag/flow/hierarchical_merger/schema.py create mode 100644 rag/flow/splitter/__init__.py rename rag/flow/{chunker => splitter}/schema.py (86%) create mode 100644 rag/flow/splitter/splitter.py create mode 100644 rag/flow/tests/dsl_examples/hierarchical_merger.json create mode 100644 rag/prompts/toc_detection.md create mode 100644 rag/prompts/toc_extraction.md create mode 100644 rag/prompts/toc_extraction_continue.md create mode 100644 rag/prompts/toc_index.md create mode 100644 web/src/assets/svg/data-flow/data-icon-bri.svg create mode 100644 web/src/assets/svg/data-flow/data-icon.svg delete mode 100644 web/src/assets/svg/data-flow/knowledgegraph.svg create mode 100644 web/src/assets/svg/data-flow/processing-icon-bri.svg create mode 100644 web/src/assets/svg/data-flow/processing-icon.svg delete mode 100644 web/src/assets/svg/data-flow/raptor.svg create mode 100644 web/src/assets/svg/data-flow/total-files-icon-bri.svg create mode 100644 web/src/assets/svg/data-flow/total-files-icon.svg create mode 100644 web/src/components/data-pipeline-select/index.tsx create mode 100644 web/src/components/llm-setting-items/llm-form-field.tsx create mode 100644 web/src/hooks/use-dataflow-request.ts create mode 100644 web/src/pages/agents/hooks/use-selelct-filters.ts delete mode 100644 web/src/pages/data-flow/canvas/node/agent-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/categorize-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/chunker-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/email-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/extractor-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/generate-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/hierarchical-merger-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/invoke-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/iteration-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/keyword-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/logic-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/message-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/relevant-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/retrieval-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/rewrite-node.tsx create mode 100644 web/src/pages/data-flow/canvas/node/splitter-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/switch-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/template-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/tool-node.tsx delete mode 100644 web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts delete mode 100644 web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts delete mode 100644 web/src/pages/data-flow/form-sheet/single-debug-sheet/index.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/agent-tools.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/index.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/tool-command.tsx delete mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/use-update-mcp.ts delete mode 100644 web/src/pages/data-flow/form/agent-form/tool-popover/use-update-tools.ts delete mode 100644 web/src/pages/data-flow/form/agent-form/use-get-tools.ts delete mode 100644 web/src/pages/data-flow/form/agent-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/agent-form/use-watch-change.ts delete mode 100644 web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx delete mode 100644 web/src/pages/data-flow/form/begin-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx delete mode 100644 web/src/pages/data-flow/form/begin-form/query-table.tsx delete mode 100644 web/src/pages/data-flow/form/begin-form/use-edit-query.ts delete mode 100644 web/src/pages/data-flow/form/begin-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/begin-form/use-watch-change.ts delete mode 100644 web/src/pages/data-flow/form/begin-form/utils.ts delete mode 100644 web/src/pages/data-flow/form/categorize-form/dynamic-categorize.tsx delete mode 100644 web/src/pages/data-flow/form/categorize-form/dynamic-example.tsx delete mode 100644 web/src/pages/data-flow/form/categorize-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/categorize-form/use-form-schema.ts delete mode 100644 web/src/pages/data-flow/form/categorize-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/categorize-form/use-watch-change.ts delete mode 100644 web/src/pages/data-flow/form/chunker-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/code-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/code-form/next-variable.tsx delete mode 100644 web/src/pages/data-flow/form/code-form/schema.ts delete mode 100644 web/src/pages/data-flow/form/code-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/code-form/use-watch-change.ts delete mode 100644 web/src/pages/data-flow/form/components/api-key-field.tsx delete mode 100644 web/src/pages/data-flow/form/components/description-field.tsx delete mode 100644 web/src/pages/data-flow/form/components/dynamic-input-variable.tsx delete mode 100644 web/src/pages/data-flow/form/components/next-dynamic-input-variable.tsx delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/constant.ts delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/index.css delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/index.tsx delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/paste-handler-plugin.tsx delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/theme.ts delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/variable-node.tsx delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/variable-on-change-plugin.tsx delete mode 100644 web/src/pages/data-flow/form/components/prompt-editor/variable-picker-plugin.tsx delete mode 100644 web/src/pages/data-flow/form/components/query-variable.tsx delete mode 100644 web/src/pages/data-flow/form/crawler-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/email-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/exesql-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/exesql-form/use-submit-form.ts create mode 100644 web/src/pages/data-flow/form/extractor-form/index.tsx create mode 100644 web/src/pages/data-flow/form/extractor-form/use-switch-prompt.ts create mode 100644 web/src/pages/data-flow/form/hierarchical-merger-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/invoke-form/hooks.ts delete mode 100644 web/src/pages/data-flow/form/invoke-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/invoke-form/schema.ts delete mode 100644 web/src/pages/data-flow/form/invoke-form/use-edit-variable.ts delete mode 100644 web/src/pages/data-flow/form/invoke-form/variable-dialog.tsx delete mode 100644 web/src/pages/data-flow/form/invoke-form/variable-table.tsx delete mode 100644 web/src/pages/data-flow/form/iteration-form/dynamic-output.tsx delete mode 100644 web/src/pages/data-flow/form/iteration-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/iteration-form/interface.ts delete mode 100644 web/src/pages/data-flow/form/iteration-form/use-build-options.ts delete mode 100644 web/src/pages/data-flow/form/iteration-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/iteration-form/use-watch-form-change.ts delete mode 100644 web/src/pages/data-flow/form/iteration-start-from/index.tsx delete mode 100644 web/src/pages/data-flow/form/keyword-extract-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/message-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/message-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/message-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/form/parser-form/common-form-fields.tsx create mode 100644 web/src/pages/data-flow/form/parser-form/email-form-fields.tsx create mode 100644 web/src/pages/data-flow/form/parser-form/image-form-fields.tsx create mode 100644 web/src/pages/data-flow/form/parser-form/interface.ts create mode 100644 web/src/pages/data-flow/form/parser-form/pdf-form-fields.tsx create mode 100644 web/src/pages/data-flow/form/parser-form/use-set-initial-language.ts create mode 100644 web/src/pages/data-flow/form/parser-form/utils.ts create mode 100644 web/src/pages/data-flow/form/parser-form/video-form-fields.tsx delete mode 100644 web/src/pages/data-flow/form/relevant-form/hooks.ts delete mode 100644 web/src/pages/data-flow/form/relevant-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/retrieval-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/rewrite-question-form/index.tsx create mode 100644 web/src/pages/data-flow/form/splitter-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/string-transform-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/string-transform-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/string-transform-form/use-watch-form-change.ts delete mode 100644 web/src/pages/data-flow/form/switch-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/switch-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/switch-form/use-watch-change.ts delete mode 100644 web/src/pages/data-flow/form/user-fill-up-form/index.tsx delete mode 100644 web/src/pages/data-flow/form/user-fill-up-form/use-values.ts delete mode 100644 web/src/pages/data-flow/form/user-fill-up-form/use-watch-change.ts create mode 100644 web/src/pages/data-flow/hooks/use-build-options.tsx create mode 100644 web/src/pages/data-flow/hooks/use-cancel-dataflow.ts delete mode 100644 web/src/pages/data-flow/hooks/use-chat-logic.ts create mode 100644 web/src/pages/data-flow/hooks/use-download-output.ts create mode 100644 web/src/pages/data-flow/hooks/use-fetch-log.ts delete mode 100644 web/src/pages/data-flow/hooks/use-find-mcp-by-id.ts delete mode 100644 web/src/pages/data-flow/hooks/use-get-begin-query.tsx delete mode 100644 web/src/pages/data-flow/hooks/use-iteration.ts create mode 100644 web/src/pages/data-flow/hooks/use-run-dataflow.ts create mode 100644 web/src/pages/data-flow/log-sheet/dataflow-timeline.tsx create mode 100644 web/src/pages/data-flow/log-sheet/index.tsx create mode 100644 web/src/pages/data-flow/run-sheet/uploader.tsx delete mode 100644 web/src/pages/data-flow/upload-agent-dialog/index.tsx delete mode 100644 web/src/pages/data-flow/upload-agent-dialog/upload-agent-form.tsx delete mode 100644 web/src/pages/data-flow/utils/chat.ts create mode 100644 web/src/pages/dataflow-result/components/parse-editer/hook.ts create mode 100644 web/src/pages/dataflow-result/components/parse-editer/interface.ts create mode 100644 web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx create mode 100644 web/src/pages/dataflow-result/components/parse-editer/object-parser.tsx create mode 100644 web/src/pages/dataflow-result/interface.ts create mode 100644 web/src/pages/dataset/dataset-overview/hook.ts create mode 100644 web/src/pages/dataset/dataset-overview/interface.ts delete mode 100644 web/src/pages/dataset/dataset-setting/chunk-method-form.tsx create mode 100644 web/src/pages/dataset/dataset-setting/components/link-data-pipeline.tsx create mode 100644 web/src/pages/dataset/dataset-setting/components/link-data-pipline-modal.tsx delete mode 100644 web/src/pages/dataset/dataset-setting/naive.tsx create mode 100644 web/src/pages/dataset/dataset/generate-button/generate.tsx create mode 100644 web/src/pages/dataset/dataset/generate-button/hook.ts delete mode 100644 web/src/pages/dataset/dataset/generate.tsx create mode 100644 web/src/pages/dataset/process-log-modal.tsx delete mode 100644 web/src/pages/datasets/process-log-modal.tsx delete mode 100644 web/src/pages/flow/canvas/context-menu/index.less delete mode 100644 web/src/pages/flow/canvas/context-menu/index.tsx delete mode 100644 web/src/pages/flow/canvas/edge/index.less delete mode 100644 web/src/pages/flow/canvas/edge/index.tsx delete mode 100644 web/src/pages/flow/canvas/index.less delete mode 100644 web/src/pages/flow/canvas/index.tsx delete mode 100644 web/src/pages/flow/canvas/node/begin-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/card.tsx delete mode 100644 web/src/pages/flow/canvas/node/categorize-handle.tsx delete mode 100644 web/src/pages/flow/canvas/node/categorize-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/dropdown.tsx delete mode 100644 web/src/pages/flow/canvas/node/email-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/generate-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/handle-icon.tsx delete mode 100644 web/src/pages/flow/canvas/node/hooks.ts delete mode 100644 web/src/pages/flow/canvas/node/index.less delete mode 100644 web/src/pages/flow/canvas/node/index.tsx delete mode 100644 web/src/pages/flow/canvas/node/invoke-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/iteration-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/keyword-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/logic-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/message-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/node-header.tsx delete mode 100644 web/src/pages/flow/canvas/node/note-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/popover.tsx delete mode 100644 web/src/pages/flow/canvas/node/relevant-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/retrieval-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/rewrite-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/switch-node.tsx delete mode 100644 web/src/pages/flow/canvas/node/template-node.tsx delete mode 100644 web/src/pages/flow/chat/box.tsx delete mode 100644 web/src/pages/flow/chat/drawer.tsx delete mode 100644 web/src/pages/flow/chat/hooks.ts delete mode 100644 web/src/pages/flow/chat/index.less delete mode 100644 web/src/pages/flow/constant.tsx delete mode 100644 web/src/pages/flow/context.ts delete mode 100644 web/src/pages/flow/customer_service.json delete mode 100644 web/src/pages/flow/debug-content/index.less delete mode 100644 web/src/pages/flow/debug-content/index.tsx delete mode 100644 web/src/pages/flow/debug-content/popover-form.tsx delete mode 100644 web/src/pages/flow/flow-drawer/index.less delete mode 100644 web/src/pages/flow/flow-drawer/index.tsx delete mode 100644 web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx delete mode 100644 web/src/pages/flow/flow-id-modal/index.less delete mode 100644 web/src/pages/flow/flow-id-modal/index.tsx delete mode 100644 web/src/pages/flow/flow-setting/index.less delete mode 100644 web/src/pages/flow/flow-setting/index.tsx delete mode 100644 web/src/pages/flow/flow-sider/index.less delete mode 100644 web/src/pages/flow/flow-sider/index.tsx delete mode 100644 web/src/pages/flow/flow-tooltip.tsx delete mode 100644 web/src/pages/flow/form-hooks.ts delete mode 100644 web/src/pages/flow/form/akshare-form/index.tsx delete mode 100644 web/src/pages/flow/form/answer-form/index.tsx delete mode 100644 web/src/pages/flow/form/arxiv-form/index.tsx delete mode 100644 web/src/pages/flow/form/baidu-fanyi-form/index.tsx delete mode 100644 web/src/pages/flow/form/baidu-form/index.tsx delete mode 100644 web/src/pages/flow/form/begin-form/begin-dynamic-options.tsx delete mode 100644 web/src/pages/flow/form/begin-form/hooks.ts delete mode 100644 web/src/pages/flow/form/begin-form/index.less delete mode 100644 web/src/pages/flow/form/begin-form/index.tsx delete mode 100644 web/src/pages/flow/form/begin-form/paramater-modal.tsx delete mode 100644 web/src/pages/flow/form/begin-form/query-table.tsx delete mode 100644 web/src/pages/flow/form/bing-form/index.tsx delete mode 100644 web/src/pages/flow/form/categorize-form/dynamic-categorize.tsx delete mode 100644 web/src/pages/flow/form/categorize-form/hooks.ts delete mode 100644 web/src/pages/flow/form/categorize-form/index.less delete mode 100644 web/src/pages/flow/form/categorize-form/index.tsx delete mode 100644 web/src/pages/flow/form/code-form/dynamic-input-variable.tsx delete mode 100644 web/src/pages/flow/form/code-form/dynamic-output-variable.tsx delete mode 100644 web/src/pages/flow/form/code-form/index.less delete mode 100644 web/src/pages/flow/form/code-form/index.tsx delete mode 100644 web/src/pages/flow/form/components/dynamic-input-variable.tsx delete mode 100644 web/src/pages/flow/form/components/index.less delete mode 100644 web/src/pages/flow/form/concentrator-form/index.tsx delete mode 100644 web/src/pages/flow/form/crawler-form/index.tsx delete mode 100644 web/src/pages/flow/form/deepl-form/index.tsx delete mode 100644 web/src/pages/flow/form/duckduckgo-form/index.tsx delete mode 100644 web/src/pages/flow/form/email-form/index.tsx delete mode 100644 web/src/pages/flow/form/exesql-form/index.tsx delete mode 100644 web/src/pages/flow/form/generate-form/dynamic-parameters.tsx delete mode 100644 web/src/pages/flow/form/generate-form/hooks.ts delete mode 100644 web/src/pages/flow/form/generate-form/index.less delete mode 100644 web/src/pages/flow/form/generate-form/index.tsx delete mode 100644 web/src/pages/flow/form/github-form/index.tsx delete mode 100644 web/src/pages/flow/form/google-form/index.tsx delete mode 100644 web/src/pages/flow/form/google-scholar-form/index.tsx delete mode 100644 web/src/pages/flow/form/invoke-form/dynamic-variables.tsx delete mode 100644 web/src/pages/flow/form/invoke-form/hooks.ts delete mode 100644 web/src/pages/flow/form/invoke-form/index.less delete mode 100644 web/src/pages/flow/form/invoke-form/index.tsx delete mode 100644 web/src/pages/flow/form/iteration-from/index.tsx delete mode 100644 web/src/pages/flow/form/jin10-form/index.tsx delete mode 100644 web/src/pages/flow/form/keyword-extract-form/index.tsx delete mode 100644 web/src/pages/flow/form/message-form/index.less delete mode 100644 web/src/pages/flow/form/message-form/index.tsx delete mode 100644 web/src/pages/flow/form/pubmed-form/index.tsx delete mode 100644 web/src/pages/flow/form/qweather-form/index.tsx delete mode 100644 web/src/pages/flow/form/relevant-form/hooks.ts delete mode 100644 web/src/pages/flow/form/relevant-form/index.tsx delete mode 100644 web/src/pages/flow/form/retrieval-form/index.tsx delete mode 100644 web/src/pages/flow/form/rewrite-question-form/index.tsx delete mode 100644 web/src/pages/flow/form/switch-form/index.less delete mode 100644 web/src/pages/flow/form/switch-form/index.tsx delete mode 100644 web/src/pages/flow/form/template-form/index.tsx delete mode 100644 web/src/pages/flow/form/tushare-form/index.tsx delete mode 100644 web/src/pages/flow/form/wencai-form/index.tsx delete mode 100644 web/src/pages/flow/form/wikipedia-form/index.tsx delete mode 100644 web/src/pages/flow/form/yahoo-finance-form/index.tsx delete mode 100644 web/src/pages/flow/header/index.less delete mode 100644 web/src/pages/flow/header/index.tsx delete mode 100644 web/src/pages/flow/headhunter_zh.json delete mode 100644 web/src/pages/flow/history-version-modal/index.tsx delete mode 100644 web/src/pages/flow/hooks.tsx delete mode 100644 web/src/pages/flow/hooks/use-before-delete.tsx delete mode 100644 web/src/pages/flow/hooks/use-build-dsl.ts delete mode 100644 web/src/pages/flow/hooks/use-export-json.ts delete mode 100644 web/src/pages/flow/hooks/use-fetch-data.ts delete mode 100644 web/src/pages/flow/hooks/use-get-begin-query.tsx delete mode 100644 web/src/pages/flow/hooks/use-iteration.ts delete mode 100644 web/src/pages/flow/hooks/use-open-document.ts delete mode 100644 web/src/pages/flow/hooks/use-save-graph.ts delete mode 100644 web/src/pages/flow/hooks/use-set-graph.ts delete mode 100644 web/src/pages/flow/hooks/use-show-drawer.tsx delete mode 100644 web/src/pages/flow/index.tsx delete mode 100644 web/src/pages/flow/interface.ts delete mode 100644 web/src/pages/flow/interpreter.json delete mode 100644 web/src/pages/flow/json-upload-modal/index.less delete mode 100644 web/src/pages/flow/json-upload-modal/index.tsx delete mode 100644 web/src/pages/flow/list/agent-template-modal.tsx delete mode 100644 web/src/pages/flow/list/create-agent-modal.tsx delete mode 100644 web/src/pages/flow/list/flow-card/index.less delete mode 100644 web/src/pages/flow/list/flow-card/index.tsx delete mode 100644 web/src/pages/flow/list/graph-avatar.tsx delete mode 100644 web/src/pages/flow/list/hooks.ts delete mode 100644 web/src/pages/flow/list/index.less delete mode 100644 web/src/pages/flow/list/index.tsx delete mode 100644 web/src/pages/flow/mock.tsx delete mode 100644 web/src/pages/flow/operator-icon/index.less delete mode 100644 web/src/pages/flow/operator-icon/index.tsx delete mode 100644 web/src/pages/flow/retrieval_relevant_rewrite_and_generate.json delete mode 100644 web/src/pages/flow/run-drawer/index.less delete mode 100644 web/src/pages/flow/run-drawer/index.tsx delete mode 100644 web/src/pages/flow/run-drawer/popover-form.tsx delete mode 100644 web/src/pages/flow/store.ts delete mode 100644 web/src/pages/flow/utils.test.ts delete mode 100644 web/src/pages/flow/utils.ts create mode 100644 web/src/services/dataflow-service.ts create mode 100644 web/src/stories/slider-input-form-field.stories.tsx create mode 100644 web/src/utils/canvas-util.tsx create mode 100644 web/src/utils/list-filter-util.ts diff --git a/admin/exceptions.py b/admin/exceptions.py new file mode 100644 index 000000000..5e3021b41 --- /dev/null +++ b/admin/exceptions.py @@ -0,0 +1,17 @@ +class AdminException(Exception): + def __init__(self, message, code=400): + super().__init__(message) + self.code = code + self.message = message + +class UserNotFoundError(AdminException): + def __init__(self, username): + super().__init__(f"User '{username}' not found", 404) + +class UserAlreadyExistsError(AdminException): + def __init__(self, username): + super().__init__(f"User '{username}' already exists", 409) + +class CannotDeleteAdminError(AdminException): + def __init__(self): + super().__init__("Cannot delete admin account", 403) \ No newline at end of file diff --git a/agent/canvas.py b/agent/canvas.py index 664a68ad1..a22391deb 100644 --- a/agent/canvas.py +++ b/agent/canvas.py @@ -153,6 +153,16 @@ class Graph: def get_tenant_id(self): return self._tenant_id + def get_variable_value(self, exp: str) -> Any: + exp = exp.strip("{").strip("}").strip(" ").strip("{").strip("}") + if exp.find("@") < 0: + return self.globals[exp] + cpn_id, var_nm = exp.split("@") + cpn = self.get_component(cpn_id) + if not cpn: + raise Exception(f"Can't find variable: '{cpn_id}@{var_nm}'") + return cpn["obj"].output(var_nm) + class Canvas(Graph): @@ -406,16 +416,6 @@ class Canvas(Graph): return False return True - def get_variable_value(self, exp: str) -> Any: - exp = exp.strip("{").strip("}").strip(" ").strip("{").strip("}") - if exp.find("@") < 0: - return self.globals[exp] - cpn_id, var_nm = exp.split("@") - cpn = self.get_component(cpn_id) - if not cpn: - raise Exception(f"Can't find variable: '{cpn_id}@{var_nm}'") - return cpn["obj"].output(var_nm) - def get_history(self, window_size): convs = [] if window_size <= 0: diff --git a/agent/component/llm.py b/agent/component/llm.py index a7b9ae774..1e6c35c27 100644 --- a/agent/component/llm.py +++ b/agent/component/llm.py @@ -101,6 +101,8 @@ class LLM(ComponentBase): def get_input_elements(self) -> dict[str, Any]: res = self.get_input_elements_from_text(self._param.sys_prompt) + if isinstance(self._param.prompts, str): + self._param.prompts = [{"role": "user", "content": self._param.prompts}] for prompt in self._param.prompts: d = self.get_input_elements_from_text(prompt["content"]) res.update(d) @@ -112,6 +114,17 @@ class LLM(ComponentBase): def add2system_prompt(self, txt): self._param.sys_prompt += txt + def _sys_prompt_and_msg(self, msg, args): + if isinstance(self._param.prompts, str): + self._param.prompts = [{"role": "user", "content": self._param.prompts}] + for p in self._param.prompts: + if msg and msg[-1]["role"] == p["role"]: + continue + p = deepcopy(p) + p["content"] = self.string_format(p["content"], args) + msg.append(p) + return msg, self.string_format(self._param.sys_prompt, args) + def _prepare_prompt_variables(self): if self._param.visual_files_var: self.imgs = self._canvas.get_variable_value(self._param.visual_files_var) @@ -127,7 +140,6 @@ class LLM(ComponentBase): args = {} vars = self.get_input_elements() if not self._param.debug_inputs else self._param.debug_inputs - sys_prompt = self._param.sys_prompt for k, o in vars.items(): args[k] = o["value"] if not isinstance(args[k], str): @@ -137,16 +149,8 @@ class LLM(ComponentBase): args[k] = str(args[k]) self.set_input_value(k, args[k]) - msg = self._canvas.get_history(self._param.message_history_window_size)[:-1] - for p in self._param.prompts: - if msg and msg[-1]["role"] == p["role"]: - continue - msg.append(deepcopy(p)) - - sys_prompt = self.string_format(sys_prompt, args) + msg, sys_prompt = self._sys_prompt_and_msg(self._canvas.get_history(self._param.message_history_window_size)[:-1], args) user_defined_prompt, sys_prompt = self._extract_prompts(sys_prompt) - for m in msg: - m["content"] = self.string_format(m["content"], args) if self._param.cite and self._canvas.get_reference()["chunks"]: sys_prompt += citation_prompt(user_defined_prompt) diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 57de77613..c3d4dd824 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -19,15 +19,19 @@ import re import sys from functools import partial +import flask import trio from flask import request, Response from flask_login import login_required, current_user -from agent.component.llm import LLM +from agent.component import LLM +from api import settings from api.db import CanvasCategory, FileType from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService from api.db.services.document_service import DocumentService from api.db.services.file_service import FileService +from api.db.services.pipeline_operation_log_service import PipelineOperationLogService +from api.db.services.task_service import queue_dataflow, CANVAS_DEBUG_DOC_ID, TaskService from api.db.services.user_service import TenantService from api.db.services.user_canvas_version import UserCanvasVersionService from api.settings import RetCode @@ -35,10 +39,12 @@ from api.utils import get_uuid from api.utils.api_utils import get_json_result, server_error_response, validate_request, get_data_error_result from agent.canvas import Canvas from peewee import MySQLDatabase, PostgresqlDatabase -from api.db.db_models import APIToken +from api.db.db_models import APIToken, Task import time from api.utils.file_utils import filename_type, read_potential_broken_pdf +from rag.flow.pipeline import Pipeline +from rag.nlp import search from rag.utils.redis_conn import REDIS_CONN @@ -48,14 +54,6 @@ def templates(): return get_json_result(data=[c.to_dict() for c in CanvasTemplateService.query(canvas_category=CanvasCategory.Agent)]) -@manager.route('/list', methods=['GET']) # noqa: F821 -@login_required -def canvas_list(): - return get_json_result(data=sorted([c.to_dict() for c in \ - UserCanvasService.query(user_id=current_user.id, canvas_category=CanvasCategory.Agent)], key=lambda x: x["update_time"]*-1) - ) - - @manager.route('/rm', methods=['POST']) # noqa: F821 @validate_request("canvas_ids") @login_required @@ -77,9 +75,10 @@ def save(): if not isinstance(req["dsl"], str): req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False) req["dsl"] = json.loads(req["dsl"]) + cate = req.get("canvas_category", CanvasCategory.Agent) if "id" not in req: req["user_id"] = current_user.id - if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip(), canvas_category=CanvasCategory.Agent): + if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip(), canvas_category=cate): return get_data_error_result(message=f"{req['title'].strip()} already exists.") req["id"] = get_uuid() if not UserCanvasService.save(**req): @@ -148,6 +147,14 @@ def run(): if not isinstance(cvs.dsl, str): cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) + if cvs.canvas_category == CanvasCategory.DataFlow: + task_id = get_uuid() + Pipeline(cvs.dsl, tenant_id=current_user.id, doc_id=CANVAS_DEBUG_DOC_ID, task_id=task_id, flow_id=req["id"]) + ok, error_message = queue_dataflow(tenant_id=user_id, flow_id=req["id"], task_id=task_id, file=files[0], priority=0) + if not ok: + return get_data_error_result(message=error_message) + return get_json_result(data={"message_id": task_id}) + try: canvas = Canvas(cvs.dsl, current_user.id, req["id"]) except Exception as e: @@ -173,6 +180,44 @@ def run(): return resp +@manager.route('/rerun', methods=['POST']) # noqa: F821 +@validate_request("id", "dsl", "component_id") +@login_required +def rerun(): + req = request.json + doc = PipelineOperationLogService.get_documents_info(req["id"]) + if not doc: + return get_data_error_result(message="Document not found.") + doc = doc[0] + if 0 < doc["progress"] < 1: + return get_data_error_result(message=f"`{doc['name']}` is processing...") + + if settings.docStoreConn.indexExist(search.index_name(current_user.id), doc["kb_id"]): + settings.docStoreConn.delete({"doc_id": doc["id"]}, search.index_name(current_user.id), doc["kb_id"]) + doc["progress_msg"] = "" + doc["chunk_num"] = 0 + doc["token_num"] = 0 + DocumentService.clear_chunk_num_when_rerun(doc["id"]) + DocumentService.update_by_id(id, doc) + TaskService.filter_delete([Task.doc_id == id]) + + dsl = req["dsl"] + dsl["path"] = [req["component_id"]] + PipelineOperationLogService.update_by_id(req["id"], {"dsl": dsl}) + queue_dataflow(tenant_id=current_user.id, flow_id=req["id"], task_id=get_uuid(), doc_id=doc["id"], priority=0, rerun=True) + return get_json_result(data=True) + + +@manager.route('/cancel/', methods=['PUT']) # noqa: F821 +@login_required +def cancel(task_id): + try: + REDIS_CONN.set(f"{task_id}-cancel", "x") + except Exception as e: + logging.exception(e) + return get_json_result(data=True) + + @manager.route('/reset', methods=['POST']) # noqa: F821 @validate_request("id") @login_required @@ -399,22 +444,32 @@ def getversion( version_id): return get_json_result(data=f"Error getting history file: {e}") -@manager.route('/listteam', methods=['GET']) # noqa: F821 +@manager.route('/list', methods=['GET']) # noqa: F821 @login_required def list_canvas(): keywords = request.args.get("keywords", "") - page_number = int(request.args.get("page", 1)) - items_per_page = int(request.args.get("page_size", 150)) + page_number = int(request.args.get("page", 0)) + items_per_page = int(request.args.get("page_size", 0)) orderby = request.args.get("orderby", "create_time") - desc = request.args.get("desc", True) - try: + canvas_category = request.args.get("canvas_category") + if request.args.get("desc", "true").lower() == "false": + desc = False + else: + desc = True + owner_ids = [id for id in request.args.get("owner_ids", "").strip().split(",") if id] + if not owner_ids: tenants = TenantService.get_joined_tenants_by_user_id(current_user.id) + tenants = [m["tenant_id"] for m in tenants] + tenants.append(current_user.id) canvas, total = UserCanvasService.get_by_tenant_ids( - [m["tenant_id"] for m in tenants], current_user.id, page_number, - items_per_page, orderby, desc, keywords, canvas_category=CanvasCategory.Agent) - return get_json_result(data={"canvas": canvas, "total": total}) - except Exception as e: - return server_error_response(e) + tenants, current_user.id, page_number, + items_per_page, orderby, desc, keywords, canvas_category) + else: + tenants = owner_ids + canvas, total = UserCanvasService.get_by_tenant_ids( + tenants, current_user.id, 0, + 0, orderby, desc, keywords, canvas_category) + return get_json_result(data={"canvas": canvas, "total": total}) @manager.route('/setting', methods=['POST']) # noqa: F821 @@ -499,3 +554,11 @@ def prompts(): #"context_ranking": RANK_MEMORY, "citation_guidelines": CITATION_PROMPT_TEMPLATE }) + + +@manager.route('/download', methods=['GET']) # noqa: F821 +def download(): + id = request.args.get("id") + created_by = request.args.get("created_by") + blob = FileService.get_blob(created_by, id) + return flask.make_response(blob) \ No newline at end of file diff --git a/api/apps/dataflow_app.py b/api/apps/dataflow_app.py deleted file mode 100644 index cb0fe98b3..000000000 --- a/api/apps/dataflow_app.py +++ /dev/null @@ -1,353 +0,0 @@ -# -# Copyright 2024 The InfiniFlow Authors. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import json -import re -import sys -import time -from functools import partial - -import trio -from flask import request -from flask_login import current_user, login_required - -from agent.canvas import Canvas -from agent.component.llm import LLM -from api.db import CanvasCategory, FileType -from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService -from api.db.services.document_service import DocumentService -from api.db.services.file_service import FileService -from api.db.services.task_service import queue_dataflow -from api.db.services.user_canvas_version import UserCanvasVersionService -from api.db.services.user_service import TenantService -from api.settings import RetCode -from api.utils import get_uuid -from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response, validate_request -from api.utils.file_utils import filename_type, read_potential_broken_pdf -from rag.flow.pipeline import Pipeline - - -@manager.route("/templates", methods=["GET"]) # noqa: F821 -@login_required -def templates(): - return get_json_result(data=[c.to_dict() for c in CanvasTemplateService.query(canvas_category=CanvasCategory.DataFlow)]) - - -@manager.route("/list", methods=["GET"]) # noqa: F821 -@login_required -def canvas_list(): - return get_json_result(data=sorted([c.to_dict() for c in UserCanvasService.query(user_id=current_user.id, canvas_category=CanvasCategory.DataFlow)], key=lambda x: x["update_time"] * -1)) - - -@manager.route("/rm", methods=["POST"]) # noqa: F821 -@validate_request("canvas_ids") -@login_required -def rm(): - for i in request.json["canvas_ids"]: - if not UserCanvasService.accessible(i, current_user.id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - UserCanvasService.delete_by_id(i) - return get_json_result(data=True) - - -@manager.route("/set", methods=["POST"]) # noqa: F821 -@validate_request("dsl", "title") -@login_required -def save(): - req = request.json - if not isinstance(req["dsl"], str): - req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False) - req["dsl"] = json.loads(req["dsl"]) - req["canvas_category"] = CanvasCategory.DataFlow - if "id" not in req: - req["user_id"] = current_user.id - if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip(), canvas_category=CanvasCategory.DataFlow): - return get_data_error_result(message=f"{req['title'].strip()} already exists.") - req["id"] = get_uuid() - - if not UserCanvasService.save(**req): - return get_data_error_result(message="Fail to save canvas.") - else: - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - UserCanvasService.update_by_id(req["id"], req) - # save version - UserCanvasVersionService.insert(user_canvas_id=req["id"], dsl=req["dsl"], title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S"))) - UserCanvasVersionService.delete_all_versions(req["id"]) - return get_json_result(data=req) - - -@manager.route("/get/", methods=["GET"]) # noqa: F821 -@login_required -def get(canvas_id): - if not UserCanvasService.accessible(canvas_id, current_user.id): - return get_data_error_result(message="canvas not found.") - e, c = UserCanvasService.get_by_canvas_id(canvas_id) - return get_json_result(data=c) - - -@manager.route("/run", methods=["POST"]) # noqa: F821 -@validate_request("id") -@login_required -def run(): - req = request.json - flow_id = req.get("id", "") - doc_id = req.get("doc_id", "") - if not all([flow_id, doc_id]): - return get_data_error_result(message="id and doc_id are required.") - - if not DocumentService.get_by_id(doc_id): - return get_data_error_result(message=f"Document for {doc_id} not found.") - - user_id = req.get("user_id", current_user.id) - if not UserCanvasService.accessible(flow_id, current_user.id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - - e, cvs = UserCanvasService.get_by_id(flow_id) - if not e: - return get_data_error_result(message="canvas not found.") - - if not isinstance(cvs.dsl, str): - cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) - - task_id = get_uuid() - - ok, error_message = queue_dataflow(dsl=cvs.dsl, tenant_id=user_id, doc_id=doc_id, task_id=task_id, flow_id=flow_id, priority=0) - if not ok: - return server_error_response(error_message) - - return get_json_result(data={"task_id": task_id, "flow_id": flow_id}) - - -@manager.route("/reset", methods=["POST"]) # noqa: F821 -@validate_request("id") -@login_required -def reset(): - req = request.json - flow_id = req.get("id", "") - if not flow_id: - return get_data_error_result(message="id is required.") - - if not UserCanvasService.accessible(flow_id, current_user.id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - - task_id = req.get("task_id", "") - - try: - e, user_canvas = UserCanvasService.get_by_id(req["id"]) - if not e: - return get_data_error_result(message="canvas not found.") - - dataflow = Pipeline(dsl=json.dumps(user_canvas.dsl), tenant_id=current_user.id, flow_id=flow_id, task_id=task_id) - dataflow.reset() - req["dsl"] = json.loads(str(dataflow)) - UserCanvasService.update_by_id(req["id"], {"dsl": req["dsl"]}) - return get_json_result(data=req["dsl"]) - except Exception as e: - return server_error_response(e) - - -@manager.route("/upload/", methods=["POST"]) # noqa: F821 -def upload(canvas_id): - e, cvs = UserCanvasService.get_by_canvas_id(canvas_id) - if not e: - return get_data_error_result(message="canvas not found.") - - user_id = cvs["user_id"] - - def structured(filename, filetype, blob, content_type): - nonlocal user_id - if filetype == FileType.PDF.value: - blob = read_potential_broken_pdf(blob) - - location = get_uuid() - FileService.put_blob(user_id, location, blob) - - return { - "id": location, - "name": filename, - "size": sys.getsizeof(blob), - "extension": filename.split(".")[-1].lower(), - "mime_type": content_type, - "created_by": user_id, - "created_at": time.time(), - "preview_url": None, - } - - if request.args.get("url"): - from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CrawlResult, DefaultMarkdownGenerator, PruningContentFilter - - try: - url = request.args.get("url") - filename = re.sub(r"\?.*", "", url.split("/")[-1]) - - async def adownload(): - browser_config = BrowserConfig( - headless=True, - verbose=False, - ) - async with AsyncWebCrawler(config=browser_config) as crawler: - crawler_config = CrawlerRunConfig(markdown_generator=DefaultMarkdownGenerator(content_filter=PruningContentFilter()), pdf=True, screenshot=False) - result: CrawlResult = await crawler.arun(url=url, config=crawler_config) - return result - - page = trio.run(adownload()) - if page.pdf: - if filename.split(".")[-1].lower() != "pdf": - filename += ".pdf" - return get_json_result(data=structured(filename, "pdf", page.pdf, page.response_headers["content-type"])) - - return get_json_result(data=structured(filename, "html", str(page.markdown).encode("utf-8"), page.response_headers["content-type"], user_id)) - - except Exception as e: - return server_error_response(e) - - file = request.files["file"] - try: - DocumentService.check_doc_health(user_id, file.filename) - return get_json_result(data=structured(file.filename, filename_type(file.filename), file.read(), file.content_type)) - except Exception as e: - return server_error_response(e) - - -@manager.route("/input_form", methods=["GET"]) # noqa: F821 -@login_required -def input_form(): - flow_id = request.args.get("id") - cpn_id = request.args.get("component_id") - try: - e, user_canvas = UserCanvasService.get_by_id(flow_id) - if not e: - return get_data_error_result(message="canvas not found.") - if not UserCanvasService.query(user_id=current_user.id, id=flow_id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - - dataflow = Pipeline(dsl=json.dumps(user_canvas.dsl), tenant_id=current_user.id, flow_id=flow_id, task_id="") - - return get_json_result(data=dataflow.get_component_input_form(cpn_id)) - except Exception as e: - return server_error_response(e) - - -@manager.route("/debug", methods=["POST"]) # noqa: F821 -@validate_request("id", "component_id", "params") -@login_required -def debug(): - req = request.json - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - try: - e, user_canvas = UserCanvasService.get_by_id(req["id"]) - canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id) - canvas.reset() - canvas.message_id = get_uuid() - component = canvas.get_component(req["component_id"])["obj"] - component.reset() - - if isinstance(component, LLM): - component.set_debug_inputs(req["params"]) - component.invoke(**{k: o["value"] for k, o in req["params"].items()}) - outputs = component.output() - for k in outputs.keys(): - if isinstance(outputs[k], partial): - txt = "" - for c in outputs[k](): - txt += c - outputs[k] = txt - return get_json_result(data=outputs) - except Exception as e: - return server_error_response(e) - - -# api get list version dsl of canvas -@manager.route("/getlistversion/", methods=["GET"]) # noqa: F821 -@login_required -def getlistversion(canvas_id): - try: - list = sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"] * -1) - return get_json_result(data=list) - except Exception as e: - return get_data_error_result(message=f"Error getting history files: {e}") - - -# api get version dsl of canvas -@manager.route("/getversion/", methods=["GET"]) # noqa: F821 -@login_required -def getversion(version_id): - try: - e, version = UserCanvasVersionService.get_by_id(version_id) - if version: - return get_json_result(data=version.to_dict()) - except Exception as e: - return get_json_result(data=f"Error getting history file: {e}") - - -@manager.route("/listteam", methods=["GET"]) # noqa: F821 -@login_required -def list_canvas(): - keywords = request.args.get("keywords", "") - page_number = int(request.args.get("page", 1)) - items_per_page = int(request.args.get("page_size", 150)) - orderby = request.args.get("orderby", "create_time") - desc = request.args.get("desc", True) - try: - tenants = TenantService.get_joined_tenants_by_user_id(current_user.id) - canvas, total = UserCanvasService.get_by_tenant_ids( - [m["tenant_id"] for m in tenants], current_user.id, page_number, items_per_page, orderby, desc, keywords, canvas_category=CanvasCategory.DataFlow - ) - return get_json_result(data={"canvas": canvas, "total": total}) - except Exception as e: - return server_error_response(e) - - -@manager.route("/setting", methods=["POST"]) # noqa: F821 -@validate_request("id", "title", "permission") -@login_required -def setting(): - req = request.json - req["user_id"] = current_user.id - - if not UserCanvasService.accessible(req["id"], current_user.id): - return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR) - - e, flow = UserCanvasService.get_by_id(req["id"]) - if not e: - return get_data_error_result(message="canvas not found.") - flow = flow.to_dict() - flow["title"] = req["title"] - for key in ("description", "permission", "avatar"): - if value := req.get(key): - flow[key] = value - - num = UserCanvasService.update_by_id(req["id"], flow) - return get_json_result(data=num) - - -@manager.route("/trace", methods=["GET"]) # noqa: F821 -def trace(): - dataflow_id = request.args.get("dataflow_id") - task_id = request.args.get("task_id") - if not all([dataflow_id, task_id]): - return get_data_error_result(message="dataflow_id and task_id are required.") - - e, dataflow_canvas = UserCanvasService.get_by_id(dataflow_id) - if not e: - return get_data_error_result(message="dataflow not found.") - - dsl_str = json.dumps(dataflow_canvas.dsl, ensure_ascii=False) - dataflow = Pipeline(dsl=dsl_str, tenant_id=dataflow_canvas.user_id, flow_id=dataflow_id, task_id=task_id) - log = dataflow.fetch_logs() - - return get_json_result(data=log) diff --git a/api/apps/document_app.py b/api/apps/document_app.py index cc5217506..0b3bfd6ba 100644 --- a/api/apps/document_app.py +++ b/api/apps/document_app.py @@ -33,7 +33,7 @@ from api.db.services.document_service import DocumentService, doc_upload_and_par from api.db.services.file2document_service import File2DocumentService from api.db.services.file_service import FileService from api.db.services.knowledgebase_service import KnowledgebaseService -from api.db.services.task_service import TaskService, cancel_all_task_of, queue_tasks +from api.db.services.task_service import TaskService, cancel_all_task_of, queue_tasks, queue_dataflow from api.db.services.user_service import UserTenantService from api.utils import get_uuid from api.utils.api_utils import ( @@ -187,6 +187,7 @@ def create(): "id": get_uuid(), "kb_id": kb.id, "parser_id": kb.parser_id, + "pipeline_id": kb.pipeline_id, "parser_config": kb.parser_config, "created_by": current_user.id, "type": FileType.VIRTUAL, @@ -484,8 +485,11 @@ def run(): kb_table_num_map[kb_id] = count if kb_table_num_map[kb_id] <= 0: KnowledgebaseService.delete_field_map(kb_id) - bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"]) - queue_tasks(doc, bucket, name, 0) + if doc.get("pipeline_id", ""): + queue_dataflow(tenant_id, flow_id=doc["pipeline_id"], task_id=get_uuid(), doc_id=id) + else: + bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"]) + queue_tasks(doc, bucket, name, 0) return get_json_result(data=True) except Exception as e: @@ -551,31 +555,22 @@ def get(doc_id): @manager.route("/change_parser", methods=["POST"]) # noqa: F821 @login_required -@validate_request("doc_id", "parser_id") +@validate_request("doc_id") def change_parser(): req = request.json if not DocumentService.accessible(req["doc_id"], current_user.id): return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR) - try: - e, doc = DocumentService.get_by_id(req["doc_id"]) - if not e: - return get_data_error_result(message="Document not found!") - if doc.parser_id.lower() == req["parser_id"].lower(): - if "parser_config" in req: - if req["parser_config"] == doc.parser_config: - return get_json_result(data=True) - else: - return get_json_result(data=True) - if (doc.type == FileType.VISUAL and req["parser_id"] != "picture") or (re.search(r"\.(ppt|pptx|pages)$", doc.name) and req["parser_id"] != "presentation"): - return get_data_error_result(message="Not supported yet!") + e, doc = DocumentService.get_by_id(req["doc_id"]) + if not e: + return get_data_error_result(message="Document not found!") + def reset_doc(): + nonlocal doc e = DocumentService.update_by_id(doc.id, {"parser_id": req["parser_id"], "progress": 0, "progress_msg": "", "run": TaskStatus.UNSTART.value}) if not e: return get_data_error_result(message="Document not found!") - if "parser_config" in req: - DocumentService.update_parser_config(doc.id, req["parser_config"]) if doc.token_num > 0: e = DocumentService.increment_chunk_num(doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1, doc.process_duration * -1) if not e: @@ -586,6 +581,26 @@ def change_parser(): if settings.docStoreConn.indexExist(search.index_name(tenant_id), doc.kb_id): settings.docStoreConn.delete({"doc_id": doc.id}, search.index_name(tenant_id), doc.kb_id) + try: + if "pipeline_id" in req: + if doc.pipeline_id == req["pipeline_id"]: + return get_json_result(data=True) + DocumentService.update_by_id(doc.id, {"pipeline_id": req["pipeline_id"]}) + reset_doc() + return get_json_result(data=True) + + if doc.parser_id.lower() == req["parser_id"].lower(): + if "parser_config" in req: + if req["parser_config"] == doc.parser_config: + return get_json_result(data=True) + else: + return get_json_result(data=True) + + if (doc.type == FileType.VISUAL and req["parser_id"] != "picture") or (re.search(r"\.(ppt|pptx|pages)$", doc.name) and req["parser_id"] != "presentation"): + return get_data_error_result(message="Not supported yet!") + if "parser_config" in req: + DocumentService.update_parser_config(doc.id, req["parser_config"]) + reset_doc() return get_json_result(data=True) except Exception as e: return server_error_response(e) diff --git a/api/apps/file_app.py b/api/apps/file_app.py index 30aff0cdd..a6e0b4a7a 100644 --- a/api/apps/file_app.py +++ b/api/apps/file_app.py @@ -179,9 +179,6 @@ def list_files(): if not e: return get_data_error_result(message="Folder not found!") - if not check_file_team_permission(file, current_user.id): - return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR) - files, total = FileService.get_by_pf_id( current_user.id, pf_id, page_number, items_per_page, orderby, desc, keywords) @@ -213,9 +210,6 @@ def get_parent_folder(): if not e: return get_data_error_result(message="Folder not found!") - if not check_file_team_permission(file, current_user.id): - return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR) - parent_folder = FileService.get_parent_folder(file_id) return get_json_result(data={"parent_folder": parent_folder.to_json()}) except Exception as e: @@ -231,9 +225,6 @@ def get_all_parent_folders(): if not e: return get_data_error_result(message="Folder not found!") - if not check_file_team_permission(file, current_user.id): - return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR) - parent_folders = FileService.get_all_parent_folders(file_id) parent_folders_res = [] for parent_folder in parent_folders: diff --git a/api/apps/kb_app.py b/api/apps/kb_app.py index f3c99bfba..0c56b15ad 100644 --- a/api/apps/kb_app.py +++ b/api/apps/kb_app.py @@ -14,18 +14,21 @@ # limitations under the License. # import json +import logging from flask import request from flask_login import login_required, current_user from api.db.services import duplicate_name -from api.db.services.document_service import DocumentService +from api.db.services.document_service import DocumentService, queue_raptor_o_graphrag_tasks from api.db.services.file2document_service import File2DocumentService from api.db.services.file_service import FileService +from api.db.services.pipeline_operation_log_service import PipelineOperationLogService +from api.db.services.task_service import TaskService, GRAPH_RAPTOR_FAKE_DOC_ID from api.db.services.user_service import TenantService, UserTenantService -from api.utils.api_utils import server_error_response, get_data_error_result, validate_request, not_allowed_parameters, active_required +from api.utils.api_utils import get_error_data_result, server_error_response, get_data_error_result, validate_request, not_allowed_parameters from api.utils import get_uuid -from api.db import StatusEnum, FileSource +from api.db import PipelineTaskType, StatusEnum, FileSource, VALID_FILE_TYPES, VALID_TASK_STATUS from api.db.services.knowledgebase_service import KnowledgebaseService from api.db.db_models import File from api.utils.api_utils import get_json_result @@ -38,7 +41,6 @@ from rag.utils.storage_factory import STORAGE_IMPL @manager.route('/create', methods=['post']) # noqa: F821 @login_required -@active_required @validate_request("name") def create(): req = request.json @@ -62,10 +64,39 @@ def create(): req["name"] = dataset_name req["tenant_id"] = current_user.id req["created_by"] = current_user.id + if not req.get("parser_id"): + req["parser_id"] = "naive" e, t = TenantService.get_by_id(current_user.id) if not e: return get_data_error_result(message="Tenant not found.") - req["embd_id"] = t.embd_id + req["parser_config"] = { + "layout_recognize": "DeepDOC", + "chunk_token_num": 512, + "delimiter": "\n", + "auto_keywords": 0, + "auto_questions": 0, + "html4excel": False, + "topn_tags": 3, + "raptor": { + "use_raptor": True, + "prompt": "Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following:\n {cluster_content}\nThe above is the content you need to summarize.", + "max_token": 256, + "threshold": 0.1, + "max_cluster": 64, + "random_seed": 0 + }, + "graphrag": { + "use_graphrag": True, + "entity_types": [ + "organization", + "person", + "geo", + "event", + "category" + ], + "method": "light" + } + } if not KnowledgebaseService.save(**req): return get_data_error_result() return get_json_result(data={"kb_id": req["id"]}) @@ -396,3 +427,352 @@ def get_basic_info(): basic_info = DocumentService.knowledgebase_basic_info(kb_id) return get_json_result(data=basic_info) + + +@manager.route("/list_pipeline_logs", methods=["POST"]) # noqa: F821 +@login_required +def list_pipeline_logs(): + kb_id = request.args.get("kb_id") + if not kb_id: + return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR) + + keywords = request.args.get("keywords", "") + + page_number = int(request.args.get("page", 0)) + items_per_page = int(request.args.get("page_size", 0)) + orderby = request.args.get("orderby", "create_time") + if request.args.get("desc", "true").lower() == "false": + desc = False + else: + desc = True + create_date_from = request.args.get("create_date_from", "") + create_date_to = request.args.get("create_date_to", "") + if create_date_to > create_date_from: + return get_data_error_result(message="Create data filter is abnormal.") + + req = request.get_json() + + operation_status = req.get("operation_status", []) + if operation_status: + invalid_status = {s for s in operation_status if s not in VALID_TASK_STATUS} + if invalid_status: + return get_data_error_result(message=f"Invalid filter operation_status status conditions: {', '.join(invalid_status)}") + + types = req.get("types", []) + if types: + invalid_types = {t for t in types if t not in VALID_FILE_TYPES} + if invalid_types: + return get_data_error_result(message=f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}") + + suffix = req.get("suffix", []) + + try: + logs, tol = PipelineOperationLogService.get_file_logs_by_kb_id(kb_id, page_number, items_per_page, orderby, desc, keywords, operation_status, types, suffix, create_date_from, create_date_to) + return get_json_result(data={"total": tol, "logs": logs}) + except Exception as e: + return server_error_response(e) + + +@manager.route("/list_pipeline_dataset_logs", methods=["POST"]) # noqa: F821 +@login_required +def list_pipeline_dataset_logs(): + kb_id = request.args.get("kb_id") + if not kb_id: + return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR) + + page_number = int(request.args.get("page", 0)) + items_per_page = int(request.args.get("page_size", 0)) + orderby = request.args.get("orderby", "create_time") + if request.args.get("desc", "true").lower() == "false": + desc = False + else: + desc = True + create_date_from = request.args.get("create_date_from", "") + create_date_to = request.args.get("create_date_to", "") + if create_date_to > create_date_from: + return get_data_error_result(message="Create data filter is abnormal.") + + req = request.get_json() + + operation_status = req.get("operation_status", []) + if operation_status: + invalid_status = {s for s in operation_status if s not in VALID_TASK_STATUS} + if invalid_status: + return get_data_error_result(message=f"Invalid filter operation_status status conditions: {', '.join(invalid_status)}") + + try: + logs, tol = PipelineOperationLogService.get_dataset_logs_by_kb_id(kb_id, page_number, items_per_page, orderby, desc, operation_status, create_date_from, create_date_to) + return get_json_result(data={"total": tol, "logs": logs}) + except Exception as e: + return server_error_response(e) + + +@manager.route("/delete_pipeline_logs", methods=["POST"]) # noqa: F821 +@login_required +def delete_pipeline_logs(): + kb_id = request.args.get("kb_id") + if not kb_id: + return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR) + + req = request.get_json() + log_ids = req.get("log_ids", []) + + PipelineOperationLogService.delete_by_ids(log_ids) + + return get_json_result(data=True) + + +@manager.route("/pipeline_log_detail", methods=["GET"]) # noqa: F821 +@login_required +def pipeline_log_detail(): + log_id = request.args.get("log_id") + if not log_id: + return get_json_result(data=False, message='Lack of "Pipeline log ID"', code=settings.RetCode.ARGUMENT_ERROR) + + ok, log = PipelineOperationLogService.get_by_id(log_id) + if not ok: + return get_data_error_result(message="Invalid pipeline log ID") + + return get_json_result(data=log.to_dict()) + + +@manager.route("/run_graphrag", methods=["POST"]) # noqa: F821 +@login_required +def run_graphrag(): + req = request.json + + kb_id = req.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_error_data_result(message="Invalid Knowledgebase ID") + + task_id = kb.graphrag_task_id + if task_id: + ok, task = TaskService.get_by_id(task_id) + if not ok: + logging.warning(f"A valid GraphRAG task id is expected for kb {kb_id}") + + if task and task.progress not in [-1, 1]: + return get_error_data_result(message=f"Task {task_id} in progress with status {task.progress}. A Graph Task is already running.") + + documents, _ = DocumentService.get_by_kb_id( + kb_id=kb_id, + page_number=0, + items_per_page=0, + orderby="create_time", + desc=False, + keywords="", + run_status=[], + types=[], + suffix=[], + ) + if not documents: + return get_error_data_result(message=f"No documents in Knowledgebase {kb_id}") + + sample_document = documents[0] + document_ids = [document["id"] for document in documents] + + task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="graphrag", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids)) + + if not KnowledgebaseService.update_by_id(kb.id, {"graphrag_task_id": task_id}): + logging.warning(f"Cannot save graphrag_task_id for kb {kb_id}") + + return get_json_result(data={"graphrag_task_id": task_id}) + + +@manager.route("/trace_graphrag", methods=["GET"]) # noqa: F821 +@login_required +def trace_graphrag(): + kb_id = request.args.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_error_data_result(message="Invalid Knowledgebase ID") + + task_id = kb.graphrag_task_id + if not task_id: + return get_json_result(data={}) + + ok, task = TaskService.get_by_id(task_id) + if not ok: + return get_error_data_result(message="GraphRAG Task Not Found or Error Occurred") + + return get_json_result(data=task.to_dict()) + + +@manager.route("/run_raptor", methods=["POST"]) # noqa: F821 +@login_required +def run_raptor(): + req = request.json + + kb_id = req.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_error_data_result(message="Invalid Knowledgebase ID") + + task_id = kb.raptor_task_id + if task_id: + ok, task = TaskService.get_by_id(task_id) + if not ok: + logging.warning(f"A valid RAPTOR task id is expected for kb {kb_id}") + + if task and task.progress not in [-1, 1]: + return get_error_data_result(message=f"Task {task_id} in progress with status {task.progress}. A RAPTOR Task is already running.") + + documents, _ = DocumentService.get_by_kb_id( + kb_id=kb_id, + page_number=0, + items_per_page=0, + orderby="create_time", + desc=False, + keywords="", + run_status=[], + types=[], + suffix=[], + ) + if not documents: + return get_error_data_result(message=f"No documents in Knowledgebase {kb_id}") + + sample_document = documents[0] + document_ids = [document["id"] for document in documents] + + task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="raptor", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids)) + + if not KnowledgebaseService.update_by_id(kb.id, {"raptor_task_id": task_id}): + logging.warning(f"Cannot save raptor_task_id for kb {kb_id}") + + return get_json_result(data={"raptor_task_id": task_id}) + + +@manager.route("/trace_raptor", methods=["GET"]) # noqa: F821 +@login_required +def trace_raptor(): + kb_id = request.args.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_error_data_result(message="Invalid Knowledgebase ID") + + task_id = kb.raptor_task_id + if not task_id: + return get_json_result(data={}) + + ok, task = TaskService.get_by_id(task_id) + if not ok: + return get_error_data_result(message="RAPTOR Task Not Found or Error Occurred") + + return get_json_result(data=task.to_dict()) + + +@manager.route("/run_mindmap", methods=["POST"]) # noqa: F821 +@login_required +def run_mindmap(): + req = request.json + + kb_id = req.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_error_data_result(message="Invalid Knowledgebase ID") + + task_id = kb.mindmap_task_id + if task_id: + ok, task = TaskService.get_by_id(task_id) + if not ok: + logging.warning(f"A valid Mindmap task id is expected for kb {kb_id}") + + if task and task.progress not in [-1, 1]: + return get_error_data_result(message=f"Task {task_id} in progress with status {task.progress}. A Mindmap Task is already running.") + + documents, _ = DocumentService.get_by_kb_id( + kb_id=kb_id, + page_number=0, + items_per_page=0, + orderby="create_time", + desc=False, + keywords="", + run_status=[], + types=[], + suffix=[], + ) + if not documents: + return get_error_data_result(message=f"No documents in Knowledgebase {kb_id}") + + sample_document = documents[0] + document_ids = [document["id"] for document in documents] + + task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="mindmap", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids)) + + if not KnowledgebaseService.update_by_id(kb.id, {"mindmap_task_id": task_id}): + logging.warning(f"Cannot save mindmap_task_id for kb {kb_id}") + + return get_json_result(data={"mindmap_task_id": task_id}) + + +@manager.route("/trace_mindmap", methods=["GET"]) # noqa: F821 +@login_required +def trace_mindmap(): + kb_id = request.args.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_error_data_result(message="Invalid Knowledgebase ID") + + task_id = kb.mindmap_task_id + if not task_id: + return get_json_result(data={}) + + ok, task = TaskService.get_by_id(task_id) + if not ok: + return get_error_data_result(message="Mindmap Task Not Found or Error Occurred") + + return get_json_result(data=task.to_dict()) + + +@manager.route("/unbind_task", methods=["DELETE"]) # noqa: F821 +@login_required +def delete_kb_task(): + kb_id = request.args.get("kb_id", "") + if not kb_id: + return get_error_data_result(message='Lack of "KB ID"') + ok, kb = KnowledgebaseService.get_by_id(kb_id) + if not ok: + return get_json_result(data=True) + + pipeline_task_type = request.args.get("pipeline_task_type", "") + if not pipeline_task_type or pipeline_task_type not in [PipelineTaskType.GRAPH_RAG, PipelineTaskType.RAPTOR, PipelineTaskType.MINDMAP]: + return get_error_data_result(message="Invalid task type") + + match pipeline_task_type: + case PipelineTaskType.GRAPH_RAG: + settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation"]}, search.index_name(kb.tenant_id), kb_id) + kb_task_id = "graphrag_task_id" + kb_task_finish_at = "graphrag_task_finish_at" + case PipelineTaskType.RAPTOR: + kb_task_id = "raptor_task_id" + kb_task_finish_at = "raptor_task_finish_at" + case PipelineTaskType.MINDMAP: + kb_task_id = "mindmap_task_id" + kb_task_finish_at = "mindmap_task_finish_at" + case _: + return get_error_data_result(message="Internal Error: Invalid task type") + + ok = KnowledgebaseService.update_by_id(kb_id, {kb_task_id: "", kb_task_finish_at: None}) + if not ok: + return server_error_response(f"Internal error: cannot delete task {pipeline_task_type}") + + return get_json_result(data=True) diff --git a/api/db/__init__.py b/api/db/__init__.py index 155eefd33..6e924349c 100644 --- a/api/db/__init__.py +++ b/api/db/__init__.py @@ -127,4 +127,15 @@ class MCPServerType(StrEnum): VALID_MCP_SERVER_TYPES = {MCPServerType.SSE, MCPServerType.STREAMABLE_HTTP} +class PipelineTaskType(StrEnum): + PARSE = "Parse" + DOWNLOAD = "Download" + RAPTOR = "RAPTOR" + GRAPH_RAG = "GraphRAG" + MINDMAP = "Mindmap" + + +VALID_PIPELINE_TASK_TYPES = {PipelineTaskType.PARSE, PipelineTaskType.DOWNLOAD, PipelineTaskType.RAPTOR, PipelineTaskType.GRAPH_RAG, PipelineTaskType.MINDMAP} + + KNOWLEDGEBASE_FOLDER_NAME=".knowledgebase" diff --git a/api/db/db_models.py b/api/db/db_models.py index 1ff6dfc96..7f2e35497 100644 --- a/api/db/db_models.py +++ b/api/db/db_models.py @@ -684,8 +684,17 @@ class Knowledgebase(DataBaseModel): vector_similarity_weight = FloatField(default=0.3, index=True) parser_id = CharField(max_length=32, null=False, help_text="default parser ID", default=ParserType.NAIVE.value, index=True) + pipeline_id = CharField(max_length=32, null=True, help_text="Pipeline ID", index=True) parser_config = JSONField(null=False, default={"pages": [[1, 1000000]]}) pagerank = IntegerField(default=0, index=False) + + graphrag_task_id = CharField(max_length=32, null=True, help_text="Graph RAG task ID", index=True) + graphrag_task_finish_at = DateTimeField(null=True) + raptor_task_id = CharField(max_length=32, null=True, help_text="RAPTOR task ID", index=True) + raptor_task_finish_at = DateTimeField(null=True) + mindmap_task_id = CharField(max_length=32, null=True, help_text="Mindmap task ID", index=True) + mindmap_task_finish_at = DateTimeField(null=True) + status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True) def __str__(self): @@ -700,6 +709,7 @@ class Document(DataBaseModel): thumbnail = TextField(null=True, help_text="thumbnail base64 string") kb_id = CharField(max_length=256, null=False, index=True) parser_id = CharField(max_length=32, null=False, help_text="default parser ID", index=True) + pipeline_id = CharField(max_length=32, null=True, help_text="pipleline ID", index=True) parser_config = JSONField(null=False, default={"pages": [[1, 1000000]]}) source_type = CharField(max_length=128, null=False, default="local", help_text="where dose this document come from", index=True) type = CharField(max_length=32, null=False, help_text="file extension", index=True) @@ -942,6 +952,32 @@ class Search(DataBaseModel): db_table = "search" +class PipelineOperationLog(DataBaseModel): + id = CharField(max_length=32, primary_key=True) + document_id = CharField(max_length=32, index=True) + tenant_id = CharField(max_length=32, null=False, index=True) + kb_id = CharField(max_length=32, null=False, index=True) + pipeline_id = CharField(max_length=32, null=True, help_text="Pipeline ID", index=True) + pipeline_title = CharField(max_length=32, null=True, help_text="Pipeline title", index=True) + parser_id = CharField(max_length=32, null=False, help_text="Parser ID", index=True) + document_name = CharField(max_length=255, null=False, help_text="File name") + document_suffix = CharField(max_length=255, null=False, help_text="File suffix") + document_type = CharField(max_length=255, null=False, help_text="Document type") + source_from = CharField(max_length=255, null=False, help_text="Source") + progress = FloatField(default=0, index=True) + progress_msg = TextField(null=True, help_text="process message", default="") + process_begin_at = DateTimeField(null=True, index=True) + process_duration = FloatField(default=0) + dsl = JSONField(null=True, default=dict) + task_type = CharField(max_length=32, null=False, default="") + operation_status = CharField(max_length=32, null=False, help_text="Operation status") + avatar = TextField(null=True, help_text="avatar base64 string") + status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True) + + class Meta: + db_table = "pipeline_operation_log" + + def migrate_db(): logging.disable(logging.ERROR) migrator = DatabaseMigrator[settings.DATABASE_TYPE.upper()].value(DB) @@ -1058,7 +1094,6 @@ def migrate_db(): migrate(migrator.add_column("dialog", "meta_data_filter", JSONField(null=True, default={}))) except Exception: pass - try: migrate(migrator.alter_column_type("canvas_template", "title", JSONField(null=True, default=dict, help_text="Canvas title"))) except Exception: @@ -1075,4 +1110,36 @@ def migrate_db(): migrate(migrator.add_column("canvas_template", "canvas_category", CharField(max_length=32, null=False, default="agent_canvas", help_text="agent_canvas|dataflow_canvas", index=True))) except Exception: pass + try: + migrate(migrator.add_column("knowledgebase", "pipeline_id", CharField(max_length=32, null=True, help_text="Pipeline ID", index=True))) + except Exception: + pass + try: + migrate(migrator.add_column("document", "pipeline_id", CharField(max_length=32, null=True, help_text="Pipeline ID", index=True))) + except Exception: + pass + try: + migrate(migrator.add_column("knowledgebase", "graphrag_task_id", CharField(max_length=32, null=True, help_text="Gragh RAG task ID", index=True))) + except Exception: + pass + try: + migrate(migrator.add_column("knowledgebase", "raptor_task_id", CharField(max_length=32, null=True, help_text="RAPTOR task ID", index=True))) + except Exception: + pass + try: + migrate(migrator.add_column("knowledgebase", "graphrag_task_finish_at", DateTimeField(null=True))) + except Exception: + pass + try: + migrate(migrator.add_column("knowledgebase", "raptor_task_finish_at", CharField(null=True))) + except Exception: + pass + try: + migrate(migrator.add_column("knowledgebase", "mindmap_task_id", CharField(max_length=32, null=True, help_text="Mindmap task ID", index=True))) + except Exception: + pass + try: + migrate(migrator.add_column("knowledgebase", "mindmap_task_finish_at", CharField(null=True))) + except Exception: + pass logging.disable(logging.NOTSET) diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index 7b2150c3c..f72c6f92a 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -126,7 +126,7 @@ class UserCanvasService(CommonService): @DB.connection_context() def get_by_tenant_ids(cls, joined_tenant_ids, user_id, page_number, items_per_page, - orderby, desc, keywords, canvas_category=CanvasCategory.Agent, + orderby, desc, keywords, canvas_category=None ): fields = [ cls.model.id, @@ -135,6 +135,7 @@ class UserCanvasService(CommonService): cls.model.dsl, cls.model.description, cls.model.permission, + cls.model.user_id.alias("tenant_id"), User.nickname, User.avatar.alias('tenant_avatar'), cls.model.update_time, @@ -142,24 +143,26 @@ class UserCanvasService(CommonService): ] if keywords: agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where( - ((cls.model.user_id.in_(joined_tenant_ids) & (cls.model.permission == - TenantPermission.TEAM.value)) | ( - cls.model.user_id == user_id)), - (fn.LOWER(cls.model.title).contains(keywords.lower())) + cls.model.user_id.in_(joined_tenant_ids), + fn.LOWER(cls.model.title).contains(keywords.lower()) + #(((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id)), + #(fn.LOWER(cls.model.title).contains(keywords.lower())) ) else: agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where( - ((cls.model.user_id.in_(joined_tenant_ids) & (cls.model.permission == - TenantPermission.TEAM.value)) | ( - cls.model.user_id == user_id)) + cls.model.user_id.in_(joined_tenant_ids) + #(((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id)) ) - agents = agents.where(cls.model.canvas_category == canvas_category) + if canvas_category: + agents = agents.where(cls.model.canvas_category == canvas_category) if desc: agents = agents.order_by(cls.model.getter_by(orderby).desc()) else: agents = agents.order_by(cls.model.getter_by(orderby).asc()) + count = agents.count() - agents = agents.paginate(page_number, items_per_page) + if page_number and items_per_page: + agents = agents.paginate(page_number, items_per_page) return list(agents.dicts()), count @classmethod diff --git a/api/db/services/document_service.py b/api/db/services/document_service.py index 9624bf4dc..af2b08f24 100644 --- a/api/db/services/document_service.py +++ b/api/db/services/document_service.py @@ -24,12 +24,13 @@ from io import BytesIO import trio import xxhash -from peewee import fn, Case +from peewee import fn, Case, JOIN from api import settings from api.constants import IMG_BASE64_PREFIX, FILE_NAME_LEN_LIMIT -from api.db import FileType, LLMType, ParserType, StatusEnum, TaskStatus, UserTenantRole -from api.db.db_models import DB, Document, Knowledgebase, Task, Tenant, UserTenant, File2Document, File +from api.db import FileType, LLMType, ParserType, StatusEnum, TaskStatus, UserTenantRole, CanvasCategory +from api.db.db_models import DB, Document, Knowledgebase, Task, Tenant, UserTenant, File2Document, File, UserCanvas, \ + User from api.db.db_utils import bulk_insert_into_db from api.db.services.common_service import CommonService from api.db.services.knowledgebase_service import KnowledgebaseService @@ -51,6 +52,7 @@ class DocumentService(CommonService): cls.model.thumbnail, cls.model.kb_id, cls.model.parser_id, + cls.model.pipeline_id, cls.model.parser_config, cls.model.source_type, cls.model.type, @@ -79,7 +81,10 @@ class DocumentService(CommonService): def get_list(cls, kb_id, page_number, items_per_page, orderby, desc, keywords, id, name): fields = cls.get_cls_model_fields() - docs = cls.model.select(*fields).join(File2Document, on = (File2Document.document_id == cls.model.id)).join(File, on = (File.id == File2Document.file_id)).where(cls.model.kb_id == kb_id) + docs = cls.model.select(*[*fields, UserCanvas.title]).join(File2Document, on = (File2Document.document_id == cls.model.id))\ + .join(File, on = (File.id == File2Document.file_id))\ + .join(UserCanvas, on = ((cls.model.pipeline_id == UserCanvas.id) & (UserCanvas.canvas_category == CanvasCategory.DataFlow.value)), join_type=JOIN.LEFT_OUTER)\ + .where(cls.model.kb_id == kb_id) if id: docs = docs.where( cls.model.id == id) @@ -117,12 +122,22 @@ class DocumentService(CommonService): orderby, desc, keywords, run_status, types, suffix): fields = cls.get_cls_model_fields() if keywords: - docs = cls.model.select(*fields).join(File2Document, on=(File2Document.document_id == cls.model.id)).join(File, on=(File.id == File2Document.file_id)).where( - (cls.model.kb_id == kb_id), - (fn.LOWER(cls.model.name).contains(keywords.lower())) - ) + docs = cls.model.select(*[*fields, UserCanvas.title.alias("pipeline_name"), User.nickname])\ + .join(File2Document, on=(File2Document.document_id == cls.model.id))\ + .join(File, on=(File.id == File2Document.file_id))\ + .join(UserCanvas, on=(cls.model.pipeline_id == UserCanvas.id), join_type=JOIN.LEFT_OUTER)\ + .join(User, on=(cls.model.created_by == User.id), join_type=JOIN.LEFT_OUTER)\ + .where( + (cls.model.kb_id == kb_id), + (fn.LOWER(cls.model.name).contains(keywords.lower())) + ) else: - docs = cls.model.select(*fields).join(File2Document, on=(File2Document.document_id == cls.model.id)).join(File, on=(File.id == File2Document.file_id)).where(cls.model.kb_id == kb_id) + docs = cls.model.select(*[*fields, UserCanvas.title.alias("pipeline_name"), User.nickname])\ + .join(File2Document, on=(File2Document.document_id == cls.model.id))\ + .join(UserCanvas, on=(cls.model.pipeline_id == UserCanvas.id), join_type=JOIN.LEFT_OUTER)\ + .join(File, on=(File.id == File2Document.file_id))\ + .join(User, on=(cls.model.created_by == User.id), join_type=JOIN.LEFT_OUTER)\ + .where(cls.model.kb_id == kb_id) if run_status: docs = docs.where(cls.model.run.in_(run_status)) @@ -370,8 +385,7 @@ class DocumentService(CommonService): process_duration=cls.model.process_duration + duration).where( cls.model.id == doc_id).execute() if num == 0: - raise LookupError( - "Document not found which is supposed to be there") + logging.warning("Document not found which is supposed to be there") num = Knowledgebase.update( token_num=Knowledgebase.token_num + token_num, @@ -637,6 +651,22 @@ class DocumentService(CommonService): @DB.connection_context() def update_progress(cls): docs = cls.get_unfinished_docs() + + cls._sync_progress(docs) + + + @classmethod + @DB.connection_context() + def update_progress_immediately(cls, docs:list[dict]): + if not docs: + return + + cls._sync_progress(docs) + + + @classmethod + @DB.connection_context() + def _sync_progress(cls, docs:list[dict]): for d in docs: try: tsks = Task.query(doc_id=d["id"], order_by=Task.create_time) @@ -646,8 +676,6 @@ class DocumentService(CommonService): prg = 0 finished = True bad = 0 - has_raptor = False - has_graphrag = False e, doc = DocumentService.get_by_id(d["id"]) status = doc.run # TaskStatus.RUNNING.value priority = 0 @@ -659,24 +687,14 @@ class DocumentService(CommonService): prg += t.progress if t.progress >= 0 else 0 if t.progress_msg.strip(): msg.append(t.progress_msg) - if t.task_type == "raptor": - has_raptor = True - elif t.task_type == "graphrag": - has_graphrag = True priority = max(priority, t.priority) prg /= len(tsks) if finished and bad: prg = -1 status = TaskStatus.FAIL.value elif finished: - if (d["parser_config"].get("raptor") or {}).get("use_raptor") and not has_raptor: - queue_raptor_o_graphrag_tasks(d, "raptor", priority) - prg = 0.98 * len(tsks) / (len(tsks) + 1) - elif (d["parser_config"].get("graphrag") or {}).get("use_graphrag") and not has_graphrag: - queue_raptor_o_graphrag_tasks(d, "graphrag", priority) - prg = 0.98 * len(tsks) / (len(tsks) + 1) - else: - status = TaskStatus.DONE.value + prg = 1 + status = TaskStatus.DONE.value msg = "\n".join(sorted(msg)) info = { @@ -688,7 +706,7 @@ class DocumentService(CommonService): info["progress"] = prg if msg: info["progress_msg"] = msg - if msg.endswith("created task graphrag") or msg.endswith("created task raptor"): + if msg.endswith("created task graphrag") or msg.endswith("created task raptor") or msg.endswith("created task mindmap"): info["progress_msg"] += "\n%d tasks are ahead in the queue..."%get_queue_length(priority) else: info["progress_msg"] = "%d tasks are ahead in the queue..."%get_queue_length(priority) @@ -769,7 +787,11 @@ class DocumentService(CommonService): "cancelled": int(cancelled), } -def queue_raptor_o_graphrag_tasks(doc, ty, priority): +def queue_raptor_o_graphrag_tasks(doc, ty, priority, fake_doc_id="", doc_ids=[]): + """ + You can provide a fake_doc_id to bypass the restriction of tasks at the knowledgebase level. + Optionally, specify a list of doc_ids to determine which documents participate in the task. + """ chunking_config = DocumentService.get_chunking_config(doc["id"]) hasher = xxhash.xxh64() for field in sorted(chunking_config.keys()): @@ -779,11 +801,12 @@ def queue_raptor_o_graphrag_tasks(doc, ty, priority): nonlocal doc return { "id": get_uuid(), - "doc_id": doc["id"], + "doc_id": fake_doc_id if fake_doc_id else doc["id"], "from_page": 100000000, "to_page": 100000000, "task_type": ty, - "progress_msg": datetime.now().strftime("%H:%M:%S") + " created task " + ty + "progress_msg": datetime.now().strftime("%H:%M:%S") + " created task " + ty, + "begin_at": datetime.now(), } task = new_task() @@ -792,7 +815,12 @@ def queue_raptor_o_graphrag_tasks(doc, ty, priority): hasher.update(ty.encode("utf-8")) task["digest"] = hasher.hexdigest() bulk_insert_into_db(Task, [task], True) + + if ty in ["graphrag", "raptor", "mindmap"]: + task["doc_ids"] = doc_ids + DocumentService.begin2parse(doc["id"]) assert REDIS_CONN.queue_product(get_svr_queue_name(priority), message=task), "Can't access Redis. Please check the Redis' status." + return task["id"] def get_queue_length(priority): diff --git a/api/db/services/file_service.py b/api/db/services/file_service.py index e65e34cc5..7c8d91de2 100644 --- a/api/db/services/file_service.py +++ b/api/db/services/file_service.py @@ -457,6 +457,7 @@ class FileService(CommonService): "id": doc_id, "kb_id": kb.id, "parser_id": self.get_parser(filetype, filename, kb.parser_id), + "pipeline_id": kb.pipeline_id, "parser_config": kb.parser_config, "created_by": user_id, "type": filetype, @@ -512,7 +513,7 @@ class FileService(CommonService): return ParserType.AUDIO.value if re.search(r"\.(ppt|pptx|pages)$", filename): return ParserType.PRESENTATION.value - if re.search(r"\.(eml)$", filename): + if re.search(r"\.(msg|eml)$", filename): return ParserType.EMAIL.value return default diff --git a/api/db/services/knowledgebase_service.py b/api/db/services/knowledgebase_service.py index d0bd8faa4..492c24510 100644 --- a/api/db/services/knowledgebase_service.py +++ b/api/db/services/knowledgebase_service.py @@ -15,10 +15,10 @@ # from datetime import datetime -from peewee import fn +from peewee import fn, JOIN from api.db import StatusEnum, TenantPermission -from api.db.db_models import DB, Document, Knowledgebase, Tenant, User, UserTenant +from api.db.db_models import DB, Document, Knowledgebase, User, UserTenant, UserCanvas from api.db.services.common_service import CommonService from api.utils import current_timestamp, datetime_format @@ -260,20 +260,29 @@ class KnowledgebaseService(CommonService): cls.model.token_num, cls.model.chunk_num, cls.model.parser_id, + cls.model.pipeline_id, + UserCanvas.title.alias("pipeline_name"), + UserCanvas.avatar.alias("pipeline_avatar"), cls.model.parser_config, cls.model.pagerank, + cls.model.graphrag_task_id, + cls.model.graphrag_task_finish_at, + cls.model.raptor_task_id, + cls.model.raptor_task_finish_at, + cls.model.mindmap_task_id, + cls.model.mindmap_task_finish_at, cls.model.create_time, cls.model.update_time ] - kbs = cls.model.select(*fields).join(Tenant, on=( - (Tenant.id == cls.model.tenant_id) & (Tenant.status == StatusEnum.VALID.value))).where( + kbs = cls.model.select(*fields)\ + .join(UserCanvas, on=(cls.model.pipeline_id == UserCanvas.id), join_type=JOIN.LEFT_OUTER)\ + .where( (cls.model.id == kb_id), (cls.model.status == StatusEnum.VALID.value) - ) + ).dicts() if not kbs: return - d = kbs[0].to_dict() - return d + return kbs[0] @classmethod @DB.connection_context() diff --git a/api/db/services/pipeline_operation_log_service.py b/api/db/services/pipeline_operation_log_service.py new file mode 100644 index 000000000..7bfe56c80 --- /dev/null +++ b/api/db/services/pipeline_operation_log_service.py @@ -0,0 +1,263 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import json +import logging +import os +from datetime import datetime, timedelta + +from peewee import fn + +from api.db import VALID_PIPELINE_TASK_TYPES, PipelineTaskType +from api.db.db_models import DB, Document, PipelineOperationLog +from api.db.services.canvas_service import UserCanvasService +from api.db.services.common_service import CommonService +from api.db.services.document_service import DocumentService +from api.db.services.knowledgebase_service import KnowledgebaseService +from api.db.services.task_service import GRAPH_RAPTOR_FAKE_DOC_ID +from api.utils import current_timestamp, datetime_format, get_uuid + + +class PipelineOperationLogService(CommonService): + model = PipelineOperationLog + + @classmethod + def get_file_logs_fields(cls): + return [ + cls.model.id, + cls.model.document_id, + cls.model.tenant_id, + cls.model.kb_id, + cls.model.pipeline_id, + cls.model.pipeline_title, + cls.model.parser_id, + cls.model.document_name, + cls.model.document_suffix, + cls.model.document_type, + cls.model.source_from, + cls.model.progress, + cls.model.progress_msg, + cls.model.process_begin_at, + cls.model.process_duration, + cls.model.dsl, + cls.model.task_type, + cls.model.operation_status, + cls.model.avatar, + cls.model.status, + cls.model.create_time, + cls.model.create_date, + cls.model.update_time, + cls.model.update_date, + ] + + @classmethod + def get_dataset_logs_fields(cls): + return [ + cls.model.id, + cls.model.tenant_id, + cls.model.kb_id, + cls.model.progress, + cls.model.progress_msg, + cls.model.process_begin_at, + cls.model.process_duration, + cls.model.task_type, + cls.model.operation_status, + cls.model.avatar, + cls.model.status, + cls.model.create_time, + cls.model.create_date, + cls.model.update_time, + cls.model.update_date, + ] + + @classmethod + def save(cls, **kwargs): + """ + wrap this function in a transaction + """ + sample_obj = cls.model(**kwargs).save(force_insert=True) + return sample_obj + + @classmethod + @DB.connection_context() + def create(cls, document_id, pipeline_id, task_type, fake_document_ids=[], dsl: str = "{}"): + referred_document_id = document_id + + if referred_document_id == GRAPH_RAPTOR_FAKE_DOC_ID and fake_document_ids: + referred_document_id = fake_document_ids[0] + ok, document = DocumentService.get_by_id(referred_document_id) + if not ok: + logging.warning(f"Document for referred_document_id {referred_document_id} not found") + return + DocumentService.update_progress_immediately([document.to_dict()]) + ok, document = DocumentService.get_by_id(referred_document_id) + if not ok: + logging.warning(f"Document for referred_document_id {referred_document_id} not found") + return + if document.progress not in [1, -1]: + return + operation_status = document.run + + if pipeline_id: + ok, user_pipeline = UserCanvasService.get_by_id(pipeline_id) + if not ok: + raise RuntimeError(f"Pipeline {pipeline_id} not found") + tenant_id = user_pipeline.user_id + title = user_pipeline.title + avatar = user_pipeline.avatar + else: + ok, kb_info = KnowledgebaseService.get_by_id(document.kb_id) + if not ok: + raise RuntimeError(f"Cannot find knowledge base {document.kb_id} for referred_document {referred_document_id}") + + tenant_id = kb_info.tenant_id + title = document.parser_id + avatar = document.thumbnail + + if task_type not in VALID_PIPELINE_TASK_TYPES: + raise ValueError(f"Invalid task type: {task_type}") + + if task_type in [PipelineTaskType.GRAPH_RAG, PipelineTaskType.RAPTOR, PipelineTaskType.MINDMAP]: + finish_at = document.process_begin_at + timedelta(seconds=document.process_duration) + if task_type == PipelineTaskType.GRAPH_RAG: + KnowledgebaseService.update_by_id( + document.kb_id, + {"graphrag_task_finish_at": finish_at}, + ) + elif task_type == PipelineTaskType.RAPTOR: + KnowledgebaseService.update_by_id( + document.kb_id, + {"raptor_task_finish_at": finish_at}, + ) + elif task_type == PipelineTaskType.MINDMAP: + KnowledgebaseService.update_by_id( + document.kb_id, + {"mindmap_task_finish_at": finish_at}, + ) + + log = dict( + id=get_uuid(), + document_id=document_id, # GRAPH_RAPTOR_FAKE_DOC_ID or real document_id + tenant_id=tenant_id, + kb_id=document.kb_id, + pipeline_id=pipeline_id, + pipeline_title=title, + parser_id=document.parser_id, + document_name=document.name, + document_suffix=document.suffix, + document_type=document.type, + source_from="", # TODO: add in the future + progress=document.progress, + progress_msg=document.progress_msg, + process_begin_at=document.process_begin_at, + process_duration=document.process_duration, + dsl=json.loads(dsl), + task_type=task_type, + operation_status=operation_status, + avatar=avatar, + ) + log["create_time"] = current_timestamp() + log["create_date"] = datetime_format(datetime.now()) + log["update_time"] = current_timestamp() + log["update_date"] = datetime_format(datetime.now()) + + with DB.atomic(): + obj = cls.save(**log) + + limit = int(os.getenv("PIPELINE_OPERATION_LOG_LIMIT", 1000)) + total = cls.model.select().where(cls.model.kb_id == document.kb_id).count() + + if total > limit: + keep_ids = [m.id for m in cls.model.select(cls.model.id).where(cls.model.kb_id == document.kb_id).order_by(cls.model.create_time.desc()).limit(limit)] + + deleted = cls.model.delete().where(cls.model.kb_id == document.kb_id, cls.model.id.not_in(keep_ids)).execute() + logging.info(f"[PipelineOperationLogService] Cleaned {deleted} old logs, kept latest {limit} for {document.kb_id}") + + return obj + + @classmethod + @DB.connection_context() + def record_pipeline_operation(cls, document_id, pipeline_id, task_type, fake_document_ids=[]): + return cls.create(document_id=document_id, pipeline_id=pipeline_id, task_type=task_type, fake_document_ids=fake_document_ids) + + @classmethod + @DB.connection_context() + def get_file_logs_by_kb_id(cls, kb_id, page_number, items_per_page, orderby, desc, keywords, operation_status, types, suffix, create_date_from=None, create_date_to=None): + fields = cls.get_file_logs_fields() + if keywords: + logs = cls.model.select(*fields).where((cls.model.kb_id == kb_id), (fn.LOWER(cls.model.document_name).contains(keywords.lower()))) + else: + logs = cls.model.select(*fields).where(cls.model.kb_id == kb_id) + + logs = logs.where(cls.model.document_id != GRAPH_RAPTOR_FAKE_DOC_ID) + + if operation_status: + logs = logs.where(cls.model.operation_status.in_(operation_status)) + if types: + logs = logs.where(cls.model.document_type.in_(types)) + if suffix: + logs = logs.where(cls.model.document_suffix.in_(suffix)) + if create_date_from: + logs = logs.where(cls.model.create_date >= create_date_from) + if create_date_to: + logs = logs.where(cls.model.create_date <= create_date_to) + + count = logs.count() + if desc: + logs = logs.order_by(cls.model.getter_by(orderby).desc()) + else: + logs = logs.order_by(cls.model.getter_by(orderby).asc()) + + if page_number and items_per_page: + logs = logs.paginate(page_number, items_per_page) + + return list(logs.dicts()), count + + @classmethod + @DB.connection_context() + def get_documents_info(cls, id): + fields = [Document.id, Document.name, Document.progress, Document.kb_id] + return ( + cls.model.select(*fields) + .join(Document, on=(cls.model.document_id == Document.id)) + .where( + cls.model.id == id + ) + .dicts() + ) + + @classmethod + @DB.connection_context() + def get_dataset_logs_by_kb_id(cls, kb_id, page_number, items_per_page, orderby, desc, operation_status, create_date_from=None, create_date_to=None): + fields = cls.get_dataset_logs_fields() + logs = cls.model.select(*fields).where((cls.model.kb_id == kb_id), (cls.model.document_id == GRAPH_RAPTOR_FAKE_DOC_ID)) + + if operation_status: + logs = logs.where(cls.model.operation_status.in_(operation_status)) + if create_date_from: + logs = logs.where(cls.model.create_date >= create_date_from) + if create_date_to: + logs = logs.where(cls.model.create_date <= create_date_to) + + count = logs.count() + if desc: + logs = logs.order_by(cls.model.getter_by(orderby).desc()) + else: + logs = logs.order_by(cls.model.getter_by(orderby).asc()) + + if page_number and items_per_page: + logs = logs.paginate(page_number, items_per_page) + + return list(logs.dicts()), count diff --git a/api/db/services/task_service.py b/api/db/services/task_service.py index 07cfbb600..f31494b0e 100644 --- a/api/db/services/task_service.py +++ b/api/db/services/task_service.py @@ -35,6 +35,8 @@ from rag.utils.redis_conn import REDIS_CONN from api import settings from rag.nlp import search +CANVAS_DEBUG_DOC_ID = "dataflow_x" +GRAPH_RAPTOR_FAKE_DOC_ID = "graph_raptor_x" def trim_header_by_lines(text: str, max_length) -> str: # Trim header text to maximum length while preserving line breaks @@ -70,7 +72,7 @@ class TaskService(CommonService): @classmethod @DB.connection_context() - def get_task(cls, task_id): + def get_task(cls, task_id, doc_ids=[]): """Retrieve detailed task information by task ID. This method fetches comprehensive task details including associated document, @@ -84,6 +86,10 @@ class TaskService(CommonService): dict: Task details dictionary containing all task information and related metadata. Returns None if task is not found or has exceeded retry limit. """ + doc_id = cls.model.doc_id + if doc_id == CANVAS_DEBUG_DOC_ID and doc_ids: + doc_id = doc_ids[0] + fields = [ cls.model.id, cls.model.doc_id, @@ -109,7 +115,7 @@ class TaskService(CommonService): ] docs = ( cls.model.select(*fields) - .join(Document, on=(cls.model.doc_id == Document.id)) + .join(Document, on=(doc_id == Document.id)) .join(Knowledgebase, on=(Document.kb_id == Knowledgebase.id)) .join(Tenant, on=(Knowledgebase.tenant_id == Tenant.id)) .where(cls.model.id == task_id) @@ -292,21 +298,23 @@ class TaskService(CommonService): ((prog == -1) | (prog > cls.model.progress)) ) ).execute() - return + else: + with DB.lock("update_progress", -1): + if info["progress_msg"]: + progress_msg = trim_header_by_lines(task.progress_msg + "\n" + info["progress_msg"], 3000) + cls.model.update(progress_msg=progress_msg).where(cls.model.id == id).execute() + if "progress" in info: + prog = info["progress"] + cls.model.update(progress=prog).where( + (cls.model.id == id) & + ( + (cls.model.progress != -1) & + ((prog == -1) | (prog > cls.model.progress)) + ) + ).execute() - with DB.lock("update_progress", -1): - if info["progress_msg"]: - progress_msg = trim_header_by_lines(task.progress_msg + "\n" + info["progress_msg"], 3000) - cls.model.update(progress_msg=progress_msg).where(cls.model.id == id).execute() - if "progress" in info: - prog = info["progress"] - cls.model.update(progress=prog).where( - (cls.model.id == id) & - ( - (cls.model.progress != -1) & - ((prog == -1) | (prog > cls.model.progress)) - ) - ).execute() + process_duration = (datetime.now() - task.begin_at).total_seconds() + cls.model.update(process_duration=process_duration).where(cls.model.id == id).execute() @classmethod @DB.connection_context() @@ -336,7 +344,14 @@ def queue_tasks(doc: dict, bucket: str, name: str, priority: int): - Previous task chunks may be reused if available """ def new_task(): - return {"id": get_uuid(), "doc_id": doc["id"], "progress": 0.0, "from_page": 0, "to_page": 100000000} + return { + "id": get_uuid(), + "doc_id": doc["id"], + "progress": 0.0, + "from_page": 0, + "to_page": 100000000, + "begin_at": datetime.now(), + } parse_task_array = [] @@ -349,7 +364,7 @@ def queue_tasks(doc: dict, bucket: str, name: str, priority: int): page_size = doc["parser_config"].get("task_page_size") or 12 if doc["parser_id"] == "paper": page_size = doc["parser_config"].get("task_page_size") or 22 - if doc["parser_id"] in ["one", "knowledge_graph"] or do_layout != "DeepDOC": + if doc["parser_id"] in ["one", "knowledge_graph"] or do_layout != "DeepDOC" or doc["parser_config"].get("toc", True): page_size = 10 ** 9 page_ranges = doc["parser_config"].get("pages") or [(1, 10 ** 5)] for s, e in page_ranges: @@ -478,33 +493,26 @@ def has_canceled(task_id): return False -def queue_dataflow(dsl:str, tenant_id:str, doc_id:str, task_id:str, flow_id:str, priority: int, callback=None) -> tuple[bool, str]: - """ - Returns a tuple (success: bool, error_message: str). - """ - _ = callback +def queue_dataflow(tenant_id:str, flow_id:str, task_id:str, doc_id:str=CANVAS_DEBUG_DOC_ID, file:dict=None, priority: int=0, rerun:bool=False) -> tuple[bool, str]: task = dict( - id=get_uuid() if not task_id else task_id, - doc_id=doc_id, - from_page=0, - to_page=100000000, - task_type="dataflow", - priority=priority, + id=task_id, + doc_id=doc_id, + from_page=0, + to_page=100000000, + task_type="dataflow" if not rerun else "dataflow_rerun", + priority=priority, + begin_at=datetime.now(), ) - - TaskService.model.delete().where(TaskService.model.id == task["id"]).execute() + if doc_id not in [CANVAS_DEBUG_DOC_ID, GRAPH_RAPTOR_FAKE_DOC_ID]: + TaskService.model.delete().where(TaskService.model.doc_id == doc_id).execute() + DocumentService.begin2parse(doc_id) bulk_insert_into_db(model=Task, data_source=[task], replace_on_conflict=True) - kb_id = DocumentService.get_knowledgebase_id(doc_id) - if not kb_id: - return False, f"Can't find KB of this document: {doc_id}" - - task["kb_id"] = kb_id + task["kb_id"] = DocumentService.get_knowledgebase_id(doc_id) task["tenant_id"] = tenant_id - task["task_type"] = "dataflow" - task["dsl"] = dsl - task["dataflow_id"] = get_uuid() if not flow_id else flow_id + task["dataflow_id"] = flow_id + task["file"] = file if not REDIS_CONN.queue_product( get_svr_queue_name(priority), message=task diff --git a/api/utils/api_utils.py b/api/utils/api_utils.py index 1b7452495..821ec6d31 100644 --- a/api/utils/api_utils.py +++ b/api/utils/api_utils.py @@ -705,7 +705,9 @@ TimeoutException = Union[Type[BaseException], BaseException] OnTimeoutCallback = Union[Callable[..., Any], Coroutine[Any, Any, Any]] -def timeout(seconds: float | int = None, attempts: int = 2, *, exception: Optional[TimeoutException] = None, on_timeout: Optional[OnTimeoutCallback] = None): +def timeout(seconds: float | int | str = None, attempts: int = 2, *, exception: Optional[TimeoutException] = None, on_timeout: Optional[OnTimeoutCallback] = None): + if isinstance(seconds, str): + seconds = float(seconds) def decorator(func): @wraps(func) def wrapper(*args, **kwargs): diff --git a/api/utils/base64_image.py b/api/utils/base64_image.py index d9dd4cde9..25afcf332 100644 --- a/api/utils/base64_image.py +++ b/api/utils/base64_image.py @@ -1,3 +1,56 @@ import base64 +import logging +from functools import partial +from io import BytesIO + +from PIL import Image + test_image_base64 = "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAA6ElEQVR4nO3QwQ3AIBDAsIP9d25XIC+EZE8QZc18w5l9O+AlZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBT+IYAHHLHkdEgAAAABJRU5ErkJggg==" -test_image = base64.b64decode(test_image_base64) \ No newline at end of file +test_image = base64.b64decode(test_image_base64) + + +async def image2id(d: dict, storage_put_func: partial, objname:str, bucket:str="imagetemps"): + import logging + from io import BytesIO + import trio + from rag.svr.task_executor import minio_limiter + if not d.get("image"): + return + + with BytesIO() as output_buffer: + if isinstance(d["image"], bytes): + output_buffer.write(d["image"]) + output_buffer.seek(0) + else: + # If the image is in RGBA mode, convert it to RGB mode before saving it in JPEG format. + if d["image"].mode in ("RGBA", "P"): + converted_image = d["image"].convert("RGB") + d["image"] = converted_image + try: + d["image"].save(output_buffer, format='JPEG') + except OSError as e: + logging.warning( + "Saving image exception, ignore: {}".format(str(e))) + + async with minio_limiter: + await trio.to_thread.run_sync(lambda: storage_put_func(bucket=bucket, fnm=objname, binary=output_buffer.getvalue())) + d["img_id"] = f"{bucket}-{objname}" + if not isinstance(d["image"], bytes): + d["image"].close() + del d["image"] # Remove image reference + + +def id2image(image_id:str|None, storage_get_func: partial): + if not image_id: + return + arr = image_id.split("-") + if len(arr) != 2: + return + bkt, nm = image_id.split("-") + try: + blob = storage_get_func(bucket=bkt, filename=nm) + if not blob: + return + return Image.open(BytesIO(blob)) + except Exception as e: + logging.exception(e) diff --git a/api/utils/file_utils.py b/api/utils/file_utils.py index c349453bb..63e96fb78 100644 --- a/api/utils/file_utils.py +++ b/api/utils/file_utils.py @@ -155,7 +155,7 @@ def filename_type(filename): if re.match(r".*\.pdf$", filename): return FileType.PDF.value - if re.match(r".*\.(eml|doc|docx|ppt|pptx|yml|xml|htm|json|jsonl|ldjson|csv|txt|ini|xls|xlsx|wps|rtf|hlp|pages|numbers|key|md|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|html|sql)$", filename): + if re.match(r".*\.(msg|eml|doc|docx|ppt|pptx|yml|xml|htm|json|jsonl|ldjson|csv|txt|ini|xls|xlsx|wps|rtf|hlp|pages|numbers|key|md|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|html|sql)$", filename): return FileType.DOC.value if re.match(r".*\.(wav|flac|ape|alac|wavpack|wv|mp3|aac|ogg|vorbis|opus)$", filename): diff --git a/api/utils/health.py b/api/utils/health.py new file mode 100644 index 000000000..394154b9a --- /dev/null +++ b/api/utils/health.py @@ -0,0 +1,104 @@ +from timeit import default_timer as timer + +from api import settings +from api.db.db_models import DB +from rag.utils.redis_conn import REDIS_CONN +from rag.utils.storage_factory import STORAGE_IMPL + + +def _ok_nok(ok: bool) -> str: + return "ok" if ok else "nok" + + +def check_db() -> tuple[bool, dict]: + st = timer() + try: + # lightweight probe; works for MySQL/Postgres + DB.execute_sql("SELECT 1") + return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"} + except Exception as e: + return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)} + + +def check_redis() -> tuple[bool, dict]: + st = timer() + try: + ok = bool(REDIS_CONN.health()) + return ok, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"} + except Exception as e: + return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)} + + +def check_doc_engine() -> tuple[bool, dict]: + st = timer() + try: + meta = settings.docStoreConn.health() + # treat any successful call as ok + return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", **(meta or {})} + except Exception as e: + return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)} + + +def check_storage() -> tuple[bool, dict]: + st = timer() + try: + STORAGE_IMPL.health() + return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"} + except Exception as e: + return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)} + + +def check_chat() -> tuple[bool, dict]: + st = timer() + try: + cfg = getattr(settings, "CHAT_CFG", None) + ok = bool(cfg and cfg.get("factory")) + return ok, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"} + except Exception as e: + return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)} + + +def run_health_checks() -> tuple[dict, bool]: + result: dict[str, str | dict] = {} + + db_ok, db_meta = check_db() + chat_ok, chat_meta = check_chat() + + result["db"] = _ok_nok(db_ok) + if not db_ok: + result.setdefault("_meta", {})["db"] = db_meta + + result["chat"] = _ok_nok(chat_ok) + if not chat_ok: + result.setdefault("_meta", {})["chat"] = chat_meta + + # Optional probes (do not change minimal contract but exposed for observability) + try: + redis_ok, redis_meta = check_redis() + result["redis"] = _ok_nok(redis_ok) + if not redis_ok: + result.setdefault("_meta", {})["redis"] = redis_meta + except Exception: + result["redis"] = "nok" + + try: + doc_ok, doc_meta = check_doc_engine() + result["doc_engine"] = _ok_nok(doc_ok) + if not doc_ok: + result.setdefault("_meta", {})["doc_engine"] = doc_meta + except Exception: + result["doc_engine"] = "nok" + + try: + sto_ok, sto_meta = check_storage() + result["storage"] = _ok_nok(sto_ok) + if not sto_ok: + result.setdefault("_meta", {})["storage"] = sto_meta + except Exception: + result["storage"] = "nok" + + all_ok = (result.get("db") == "ok") and (result.get("chat") == "ok") + result["status"] = "ok" if all_ok else "nok" + return result, all_ok + + diff --git a/conf/llm_factories.json b/conf/llm_factories.json index e967f9d73..e6b82f46b 100644 --- a/conf/llm_factories.json +++ b/conf/llm_factories.json @@ -5147,4 +5147,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/deepdoc/parser/pdf_parser.py b/deepdoc/parser/pdf_parser.py index 09a41ae68..ea3a87b14 100644 --- a/deepdoc/parser/pdf_parser.py +++ b/deepdoc/parser/pdf_parser.py @@ -1075,11 +1075,10 @@ class RAGFlowPdfParser: def insert_table_figures(tbls_or_figs, layout_type): def min_rectangle_distance(rect1, rect2): import math - pn1, left1, right1, top1, bottom1 = rect1 pn2, left2, right2, top2, bottom2 = rect2 if right1 >= left2 and right2 >= left1 and bottom1 >= top2 and bottom2 >= top1: - return 0 + (pn1 - pn2) * 10000 + return 0 if right1 < left2: dx = left2 - right1 elif right2 < left1: @@ -1092,20 +1091,27 @@ class RAGFlowPdfParser: dy = top1 - bottom2 else: dy = 0 - return math.sqrt(dx * dx + dy * dy) + (pn1 - pn2) * 10000 + return math.sqrt(dx*dx + dy*dy)# + (pn2-pn1)*10000 for (img, txt), poss in tbls_or_figs: bboxes = [(i, (b["page_number"], b["x0"], b["x1"], b["top"], b["bottom"])) for i, b in enumerate(self.boxes)] - dists = [(min_rectangle_distance((pn, left, right, top, bott), rect), i) for i, rect in bboxes for pn, left, right, top, bott in poss] + dists = [(min_rectangle_distance((pn, left, right, top+self.page_cum_height[pn], bott+self.page_cum_height[pn]), rect),i) for i, rect in bboxes for pn, left, right, top, bott in poss] min_i = np.argmin(dists, axis=0)[0] min_i, rect = bboxes[dists[min_i][-1]] if isinstance(txt, list): txt = "\n".join(txt) - self.boxes.insert(min_i, {"page_number": rect[0], "x0": rect[1], "x1": rect[2], "top": rect[3], "bottom": rect[4], "layout_type": layout_type, "text": txt, "image": img}) + pn, left, right, top, bott = poss[0] + if self.boxes[min_i]["bottom"] < top+self.page_cum_height[pn]: + min_i += 1 + self.boxes.insert(min_i, { + "page_number": pn+1, "x0": left, "x1": right, "top": top+self.page_cum_height[pn], "bottom": bott+self.page_cum_height[pn], "layout_type": layout_type, "text": txt, "image": img, + "positions": [[pn+1, int(left), int(right), int(top), int(bott)]] + }) for b in self.boxes: b["position_tag"] = self._line_tag(b, zoomin) b["image"] = self.crop(b["position_tag"], zoomin) + b["positions"] = [[pos[0][-1]+1, *pos[1:]] for pos in RAGFlowPdfParser.extract_positions(b["position_tag"])] insert_table_figures(tbls, "table") insert_table_figures(figs, "figure") diff --git a/graphrag/general/index.py b/graphrag/general/index.py index 9e80309f2..6d0df65bb 100644 --- a/graphrag/general/index.py +++ b/graphrag/general/index.py @@ -21,6 +21,7 @@ import networkx as nx import trio from api import settings +from api.db.services.document_service import DocumentService from api.utils import get_uuid from api.utils.api_utils import timeout from graphrag.entity_resolution import EntityResolution @@ -54,7 +55,7 @@ async def run_graphrag( start = trio.current_time() tenant_id, kb_id, doc_id = row["tenant_id"], str(row["kb_id"]), row["doc_id"] chunks = [] - for d in settings.retrievaler.chunk_list(doc_id, tenant_id, [kb_id], fields=["content_with_weight", "doc_id"]): + for d in settings.retrievaler.chunk_list(doc_id, tenant_id, [kb_id], fields=["content_with_weight", "doc_id"], sort_by_position=True): chunks.append(d["content_with_weight"]) with trio.fail_after(max(120, len(chunks) * 60 * 10) if enable_timeout_assertion else 10000000000): @@ -125,6 +126,212 @@ async def run_graphrag( return +async def run_graphrag_for_kb( + row: dict, + doc_ids: list[str], + language: str, + kb_parser_config: dict, + chat_model, + embedding_model, + callback, + *, + with_resolution: bool = True, + with_community: bool = True, + max_parallel_docs: int = 4, +) -> dict: + tenant_id, kb_id = row["tenant_id"], row["kb_id"] + enable_timeout_assertion = os.environ.get("ENABLE_TIMEOUT_ASSERTION") + start = trio.current_time() + fields_for_chunks = ["content_with_weight", "doc_id"] + + if not doc_ids: + logging.info(f"Fetching all docs for {kb_id}") + docs, _ = DocumentService.get_by_kb_id( + kb_id=kb_id, + page_number=0, + items_per_page=0, + orderby="create_time", + desc=False, + keywords="", + run_status=[], + types=[], + suffix=[], + ) + doc_ids = [doc["id"] for doc in docs] + + doc_ids = list(dict.fromkeys(doc_ids)) + if not doc_ids: + callback(msg=f"[GraphRAG] kb:{kb_id} has no processable doc_id.") + return {"ok_docs": [], "failed_docs": [], "total_docs": 0, "total_chunks": 0, "seconds": 0.0} + + def load_doc_chunks(doc_id: str) -> list[str]: + from rag.utils import num_tokens_from_string + + chunks = [] + current_chunk = "" + + for d in settings.retrievaler.chunk_list( + doc_id, + tenant_id, + [kb_id], + fields=fields_for_chunks, + sort_by_position=True, + ): + content = d["content_with_weight"] + if num_tokens_from_string(current_chunk + content) < 1024: + current_chunk += content + else: + if current_chunk: + chunks.append(current_chunk) + current_chunk = content + + if current_chunk: + chunks.append(current_chunk) + + return chunks + + all_doc_chunks: dict[str, list[str]] = {} + total_chunks = 0 + for doc_id in doc_ids: + chunks = load_doc_chunks(doc_id) + all_doc_chunks[doc_id] = chunks + total_chunks += len(chunks) + + if total_chunks == 0: + callback(msg=f"[GraphRAG] kb:{kb_id} has no available chunks in all documents, skip.") + return {"ok_docs": [], "failed_docs": doc_ids, "total_docs": len(doc_ids), "total_chunks": 0, "seconds": 0.0} + + semaphore = trio.Semaphore(max_parallel_docs) + + subgraphs: dict[str, object] = {} + failed_docs: list[tuple[str, str]] = [] # (doc_id, error) + + async def build_one(doc_id: str): + chunks = all_doc_chunks.get(doc_id, []) + if not chunks: + callback(msg=f"[GraphRAG] doc:{doc_id} has no available chunks, skip generation.") + return + + kg_extractor = LightKGExt if ("method" not in kb_parser_config.get("graphrag", {}) or kb_parser_config["graphrag"]["method"] != "general") else GeneralKGExt + + deadline = max(120, len(chunks) * 60 * 10) if enable_timeout_assertion else 10000000000 + + async with semaphore: + try: + msg = f"[GraphRAG] build_subgraph doc:{doc_id}" + callback(msg=f"{msg} start (chunks={len(chunks)}, timeout={deadline}s)") + with trio.fail_after(deadline): + sg = await generate_subgraph( + kg_extractor, + tenant_id, + kb_id, + doc_id, + chunks, + language, + kb_parser_config.get("graphrag", {}).get("entity_types", []), + chat_model, + embedding_model, + callback, + ) + if sg: + subgraphs[doc_id] = sg + callback(msg=f"{msg} done") + else: + failed_docs.append((doc_id, "subgraph is empty")) + callback(msg=f"{msg} empty") + except Exception as e: + failed_docs.append((doc_id, repr(e))) + callback(msg=f"[GraphRAG] build_subgraph doc:{doc_id} FAILED: {e!r}") + + async with trio.open_nursery() as nursery: + for doc_id in doc_ids: + nursery.start_soon(build_one, doc_id) + + ok_docs = [d for d in doc_ids if d in subgraphs] + if not ok_docs: + callback(msg=f"[GraphRAG] kb:{kb_id} no subgraphs generated successfully, end.") + now = trio.current_time() + return {"ok_docs": [], "failed_docs": failed_docs, "total_docs": len(doc_ids), "total_chunks": total_chunks, "seconds": now - start} + + kb_lock = RedisDistributedLock(f"graphrag_task_{kb_id}", lock_value="batch_merge", timeout=1200) + await kb_lock.spin_acquire() + callback(msg=f"[GraphRAG] kb:{kb_id} merge lock acquired") + + try: + union_nodes: set = set() + final_graph = None + + for doc_id in ok_docs: + sg = subgraphs[doc_id] + union_nodes.update(set(sg.nodes())) + + new_graph = await merge_subgraph( + tenant_id, + kb_id, + doc_id, + sg, + embedding_model, + callback, + ) + if new_graph is not None: + final_graph = new_graph + + if final_graph is None: + callback(msg=f"[GraphRAG] kb:{kb_id} merge finished (no in-memory graph returned).") + else: + callback(msg=f"[GraphRAG] kb:{kb_id} merge finished, graph ready.") + finally: + kb_lock.release() + + if not with_resolution and not with_community: + now = trio.current_time() + callback(msg=f"[GraphRAG] KB merge done in {now - start:.2f}s. ok={len(ok_docs)} / total={len(doc_ids)}") + return {"ok_docs": ok_docs, "failed_docs": failed_docs, "total_docs": len(doc_ids), "total_chunks": total_chunks, "seconds": now - start} + + await kb_lock.spin_acquire() + callback(msg=f"[GraphRAG] kb:{kb_id} post-merge lock acquired for resolution/community") + + try: + subgraph_nodes = set() + for sg in subgraphs.values(): + subgraph_nodes.update(set(sg.nodes())) + + if with_resolution: + await resolve_entities( + final_graph, + subgraph_nodes, + tenant_id, + kb_id, + None, + chat_model, + embedding_model, + callback, + ) + + if with_community: + await extract_community( + final_graph, + tenant_id, + kb_id, + None, + chat_model, + embedding_model, + callback, + ) + finally: + kb_lock.release() + + now = trio.current_time() + callback(msg=f"[GraphRAG] GraphRAG for KB {kb_id} done in {now - start:.2f} seconds. ok={len(ok_docs)} failed={len(failed_docs)} total_docs={len(doc_ids)} total_chunks={total_chunks}") + return { + "ok_docs": ok_docs, + "failed_docs": failed_docs, # [(doc_id, error), ...] + "total_docs": len(doc_ids), + "total_chunks": total_chunks, + "seconds": now - start, + } + + async def generate_subgraph( extractor: Extractor, tenant_id: str, diff --git a/pyproject.toml b/pyproject.toml index b1f4d059c..06981148c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "elastic-transport==8.12.0", "elasticsearch==8.12.1", "elasticsearch-dsl==8.12.0", + "extract-msg>=0.39.0", "filelock==3.15.4", "flask==3.0.3", "flask-cors==5.0.0", diff --git a/rag/app/email.py b/rag/app/email.py index d8520e43d..1affe4f25 100644 --- a/rag/app/email.py +++ b/rag/app/email.py @@ -78,7 +78,7 @@ def chunk( _add_content(msg, msg.get_content_type()) sections = TxtParser.parser_txt("\n".join(text_txt)) + [ - (line, "") for line in HtmlParser.parser_txt("\n".join(html_txt)) if line + (line, "") for line in HtmlParser.parser_txt("\n".join(html_txt), chunk_token_num=parser_config["chunk_token_num"]) if line ] st = timer() diff --git a/rag/flow/base.py b/rag/flow/base.py index 04fc9cf1a..5edc280f8 100644 --- a/rag/flow/base.py +++ b/rag/flow/base.py @@ -18,9 +18,7 @@ import os import time from functools import partial from typing import Any - import trio - from agent.component.base import ComponentBase, ComponentParamBase from api.utils.api_utils import timeout @@ -36,9 +34,9 @@ class ProcessBase(ComponentBase): def __init__(self, pipeline, id, param: ProcessParamBase): super().__init__(pipeline, id, param) if hasattr(self._canvas, "callback"): - self.callback = partial(self._canvas.callback, self.component_name) + self.callback = partial(self._canvas.callback, id) else: - self.callback = partial(lambda *args, **kwargs: None, self.component_name) + self.callback = partial(lambda *args, **kwargs: None, id) async def invoke(self, **kwargs) -> dict[str, Any]: self.set_output("_created_time", time.perf_counter()) diff --git a/rag/flow/chunker/chunker.py b/rag/flow/chunker/chunker.py deleted file mode 100644 index d7795aeb8..000000000 --- a/rag/flow/chunker/chunker.py +++ /dev/null @@ -1,212 +0,0 @@ -# -# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import random - -import trio - -from api.db import LLMType -from api.db.services.llm_service import LLMBundle -from deepdoc.parser.pdf_parser import RAGFlowPdfParser -from graphrag.utils import chat_limiter, get_llm_cache, set_llm_cache -from rag.flow.base import ProcessBase, ProcessParamBase -from rag.flow.chunker.schema import ChunkerFromUpstream -from rag.nlp import naive_merge, naive_merge_with_images -from rag.prompts.generator import keyword_extraction, question_proposal - - -class ChunkerParam(ProcessParamBase): - def __init__(self): - super().__init__() - self.method_options = [ - # General - "general", - "onetable", - # Customer Service - "q&a", - "manual", - # Recruitment - "resume", - # Education & Research - "book", - "paper", - "laws", - "presentation", - # Other - # "Tag" # TODO: Other method - ] - self.method = "general" - self.chunk_token_size = 512 - self.delimiter = "\n" - self.overlapped_percent = 0 - self.page_rank = 0 - self.auto_keywords = 0 - self.auto_questions = 0 - self.tag_sets = [] - self.llm_setting = {"llm_name": "", "lang": "Chinese"} - - def check(self): - self.check_valid_value(self.method.lower(), "Chunk method abnormal.", self.method_options) - self.check_positive_integer(self.chunk_token_size, "Chunk token size.") - self.check_nonnegative_number(self.page_rank, "Page rank value: (0, 10]") - self.check_nonnegative_number(self.auto_keywords, "Auto-keyword value: (0, 10]") - self.check_nonnegative_number(self.auto_questions, "Auto-question value: (0, 10]") - self.check_decimal_float(self.overlapped_percent, "Overlapped percentage: [0, 1)") - - def get_input_form(self) -> dict[str, dict]: - return {} - - -class Chunker(ProcessBase): - component_name = "Chunker" - - def _general(self, from_upstream: ChunkerFromUpstream): - self.callback(random.randint(1, 5) / 100.0, "Start to chunk via `General`.") - if from_upstream.output_format in ["markdown", "text", "html"]: - if from_upstream.output_format == "markdown": - payload = from_upstream.markdown_result - elif from_upstream.output_format == "text": - payload = from_upstream.text_result - else: # == "html" - payload = from_upstream.html_result - - if not payload: - payload = "" - - cks = naive_merge( - payload, - self._param.chunk_token_size, - self._param.delimiter, - self._param.overlapped_percent, - ) - return [{"text": c} for c in cks] - - # json - sections, section_images = [], [] - for o in from_upstream.json_result or []: - sections.append((o.get("text", ""), o.get("position_tag", ""))) - section_images.append(o.get("image")) - - chunks, images = naive_merge_with_images( - sections, - section_images, - self._param.chunk_token_size, - self._param.delimiter, - self._param.overlapped_percent, - ) - - return [ - { - "text": RAGFlowPdfParser.remove_tag(c), - "image": img, - "positions": RAGFlowPdfParser.extract_positions(c), - } - for c, img in zip(chunks, images) - ] - - def _q_and_a(self, from_upstream: ChunkerFromUpstream): - pass - - def _resume(self, from_upstream: ChunkerFromUpstream): - pass - - def _manual(self, from_upstream: ChunkerFromUpstream): - pass - - def _table(self, from_upstream: ChunkerFromUpstream): - pass - - def _paper(self, from_upstream: ChunkerFromUpstream): - pass - - def _book(self, from_upstream: ChunkerFromUpstream): - pass - - def _laws(self, from_upstream: ChunkerFromUpstream): - pass - - def _presentation(self, from_upstream: ChunkerFromUpstream): - pass - - def _one(self, from_upstream: ChunkerFromUpstream): - pass - - async def _invoke(self, **kwargs): - function_map = { - "general": self._general, - "q&a": self._q_and_a, - "resume": self._resume, - "manual": self._manual, - "table": self._table, - "paper": self._paper, - "book": self._book, - "laws": self._laws, - "presentation": self._presentation, - "one": self._one, - } - - try: - from_upstream = ChunkerFromUpstream.model_validate(kwargs) - except Exception as e: - self.set_output("_ERROR", f"Input error: {str(e)}") - return - - chunks = function_map[self._param.method](from_upstream) - llm_setting = self._param.llm_setting - - async def auto_keywords(): - nonlocal chunks, llm_setting - chat_mdl = LLMBundle(self._canvas._tenant_id, LLMType.CHAT, llm_name=llm_setting["llm_name"], lang=llm_setting["lang"]) - - async def doc_keyword_extraction(chat_mdl, ck, topn): - cached = get_llm_cache(chat_mdl.llm_name, ck["text"], "keywords", {"topn": topn}) - if not cached: - async with chat_limiter: - cached = await trio.to_thread.run_sync(lambda: keyword_extraction(chat_mdl, ck["text"], topn)) - set_llm_cache(chat_mdl.llm_name, ck["text"], cached, "keywords", {"topn": topn}) - if cached: - ck["keywords"] = cached.split(",") - - async with trio.open_nursery() as nursery: - for ck in chunks: - nursery.start_soon(doc_keyword_extraction, chat_mdl, ck, self._param.auto_keywords) - - async def auto_questions(): - nonlocal chunks, llm_setting - chat_mdl = LLMBundle(self._canvas._tenant_id, LLMType.CHAT, llm_name=llm_setting["llm_name"], lang=llm_setting["lang"]) - - async def doc_question_proposal(chat_mdl, d, topn): - cached = get_llm_cache(chat_mdl.llm_name, ck["text"], "question", {"topn": topn}) - if not cached: - async with chat_limiter: - cached = await trio.to_thread.run_sync(lambda: question_proposal(chat_mdl, ck["text"], topn)) - set_llm_cache(chat_mdl.llm_name, ck["text"], cached, "question", {"topn": topn}) - if cached: - d["questions"] = cached.split("\n") - - async with trio.open_nursery() as nursery: - for ck in chunks: - nursery.start_soon(doc_question_proposal, chat_mdl, ck, self._param.auto_questions) - - async with trio.open_nursery() as nursery: - if self._param.auto_questions: - nursery.start_soon(auto_questions) - if self._param.auto_keywords: - nursery.start_soon(auto_keywords) - - if self._param.page_rank: - for ck in chunks: - ck["page_rank"] = self._param.page_rank - - self.set_output("chunks", chunks) diff --git a/rag/flow/chunker/__init__.py b/rag/flow/extractor/__init__.py similarity index 100% rename from rag/flow/chunker/__init__.py rename to rag/flow/extractor/__init__.py diff --git a/rag/flow/extractor/extractor.py b/rag/flow/extractor/extractor.py new file mode 100644 index 000000000..70464148a --- /dev/null +++ b/rag/flow/extractor/extractor.py @@ -0,0 +1,63 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random +from copy import deepcopy +from agent.component.llm import LLMParam, LLM +from rag.flow.base import ProcessBase, ProcessParamBase + + +class ExtractorParam(ProcessParamBase, LLMParam): + def __init__(self): + super().__init__() + self.field_name = "" + + def check(self): + super().check() + self.check_empty(self.field_name, "Result Destination") + + +class Extractor(ProcessBase, LLM): + component_name = "Extractor" + + async def _invoke(self, **kwargs): + self.set_output("output_format", "chunks") + self.callback(random.randint(1, 5) / 100.0, "Start to generate.") + inputs = self.get_input_elements() + chunks = [] + chunks_key = "" + args = {} + for k, v in inputs.items(): + args[k] = v["value"] + if isinstance(args[k], list): + chunks = deepcopy(args[k]) + chunks_key = k + + if chunks: + prog = 0 + for i, ck in enumerate(chunks): + args[chunks_key] = ck["text"] + msg, sys_prompt = self._sys_prompt_and_msg([], args) + msg.insert(0, {"role": "system", "content": sys_prompt}) + ck[self._param.field_name] = self._generate(msg) + prog += 1./len(chunks) + if i % (len(chunks)//100+1) == 1: + self.callback(prog, f"{i+1} / {len(chunks)}") + self.set_output("chunks", chunks) + else: + msg, sys_prompt = self._sys_prompt_and_msg([], args) + msg.insert(0, {"role": "system", "content": sys_prompt}) + self.set_output("chunks", [{self._param.field_name: self._generate(msg)}]) + + diff --git a/rag/flow/extractor/schema.py b/rag/flow/extractor/schema.py new file mode 100644 index 000000000..0a9404e33 --- /dev/null +++ b/rag/flow/extractor/schema.py @@ -0,0 +1,38 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class ExtractorFromUpstream(BaseModel): + created_time: float | None = Field(default=None, alias="_created_time") + elapsed_time: float | None = Field(default=None, alias="_elapsed_time") + + name: str + file: dict | None = Field(default=None) + chunks: list[dict[str, Any]] | None = Field(default=None) + + output_format: Literal["json", "markdown", "text", "html", "chunks"] | None = Field(default=None) + + json_result: list[dict[str, Any]] | None = Field(default=None, alias="json") + markdown_result: str | None = Field(default=None, alias="markdown") + text_result: str | None = Field(default=None, alias="text") + html_result: str | None = Field(default=None, alias="html") + + model_config = ConfigDict(populate_by_name=True, extra="forbid") + + # def to_dict(self, *, exclude_none: bool = True) -> dict: + # return self.model_dump(by_alias=True, exclude_none=exclude_none) diff --git a/rag/flow/file.py b/rag/flow/file.py index 584b0ff9c..75ec211eb 100644 --- a/rag/flow/file.py +++ b/rag/flow/file.py @@ -14,10 +14,7 @@ # limitations under the License. # from api.db.services.document_service import DocumentService -from api.db.services.file2document_service import File2DocumentService -from api.db.services.file_service import FileService from rag.flow.base import ProcessBase, ProcessParamBase -from rag.utils.storage_factory import STORAGE_IMPL class FileParam(ProcessParamBase): @@ -41,10 +38,13 @@ class File(ProcessBase): self.set_output("_ERROR", f"Document({self._canvas._doc_id}) not found!") return - b, n = File2DocumentService.get_storage_address(doc_id=self._canvas._doc_id) - self.set_output("blob", STORAGE_IMPL.get(b, n)) + #b, n = File2DocumentService.get_storage_address(doc_id=self._canvas._doc_id) + #self.set_output("blob", STORAGE_IMPL.get(b, n)) self.set_output("name", doc.name) else: file = kwargs.get("file") self.set_output("name", file["name"]) - self.set_output("blob", FileService.get_blob(file["created_by"], file["id"])) + self.set_output("file", file) + #self.set_output("blob", FileService.get_blob(file["created_by"], file["id"])) + + self.callback(1, "File fetched.") diff --git a/rag/flow/hierarchical_merger/__init__.py b/rag/flow/hierarchical_merger/__init__.py new file mode 100644 index 000000000..b4663378e --- /dev/null +++ b/rag/flow/hierarchical_merger/__init__.py @@ -0,0 +1,15 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/rag/flow/hierarchical_merger/hierarchical_merger.py b/rag/flow/hierarchical_merger/hierarchical_merger.py new file mode 100644 index 000000000..dda2bcfa7 --- /dev/null +++ b/rag/flow/hierarchical_merger/hierarchical_merger.py @@ -0,0 +1,186 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import re +from copy import deepcopy +from functools import partial + +import trio + +from api.utils import get_uuid +from api.utils.base64_image import id2image, image2id +from deepdoc.parser.pdf_parser import RAGFlowPdfParser +from rag.flow.base import ProcessBase, ProcessParamBase +from rag.flow.hierarchical_merger.schema import HierarchicalMergerFromUpstream +from rag.nlp import concat_img +from rag.utils.storage_factory import STORAGE_IMPL + + +class HierarchicalMergerParam(ProcessParamBase): + def __init__(self): + super().__init__() + self.levels = [] + self.hierarchy = None + + def check(self): + self.check_empty(self.levels, "Hierarchical setups.") + self.check_empty(self.hierarchy, "Hierarchy number.") + + def get_input_form(self) -> dict[str, dict]: + return {} + + +class HierarchicalMerger(ProcessBase): + component_name = "HierarchicalMerger" + + async def _invoke(self, **kwargs): + try: + from_upstream = HierarchicalMergerFromUpstream.model_validate(kwargs) + except Exception as e: + self.set_output("_ERROR", f"Input error: {str(e)}") + return + + self.set_output("output_format", "chunks") + self.callback(random.randint(1, 5) / 100.0, "Start to merge hierarchically.") + if from_upstream.output_format in ["markdown", "text", "html"]: + if from_upstream.output_format == "markdown": + payload = from_upstream.markdown_result + elif from_upstream.output_format == "text": + payload = from_upstream.text_result + else: # == "html" + payload = from_upstream.html_result + + if not payload: + payload = "" + + lines = [ln for ln in payload.split("\n") if ln] + else: + arr = from_upstream.chunks if from_upstream.output_format == "chunks" else from_upstream.json_result + lines = [o.get("text", "") for o in arr] + sections, section_images = [], [] + for o in arr or []: + sections.append((o.get("text", ""), o.get("position_tag", ""))) + section_images.append(o.get("img_id")) + + matches = [] + for txt in lines: + good = False + for lvl, regs in enumerate(self._param.levels): + for reg in regs: + if re.search(reg, txt): + matches.append(lvl) + good = True + break + if good: + break + if not good: + matches.append(len(self._param.levels)) + assert len(matches) == len(lines), f"{len(matches)} vs. {len(lines)}" + + root = { + "level": -1, + "index": -1, + "texts": [], + "children": [] + } + for i, m in enumerate(matches): + if m == 0: + root["children"].append({ + "level": m, + "index": i, + "texts": [], + "children": [] + }) + elif m == len(self._param.levels): + def dfs(b): + if not b["children"]: + b["texts"].append(i) + else: + dfs(b["children"][-1]) + dfs(root) + else: + def dfs(b): + nonlocal m, i + if not b["children"] or m == b["level"] + 1: + b["children"].append({ + "level": m, + "index": i, + "texts": [], + "children": [] + }) + return + dfs(b["children"][-1]) + + dfs(root) + + all_pathes = [] + def dfs(n, path, depth): + nonlocal all_pathes + if not n["children"] and path: + all_pathes.append(path) + + for nn in n["children"]: + if depth < self._param.hierarchy: + _path = deepcopy(path) + else: + _path = path + _path.extend([nn["index"], *nn["texts"]]) + dfs(nn, _path, depth+1) + + if depth == self._param.hierarchy: + all_pathes.append(_path) + + for i in range(len(lines)): + print(i, lines[i]) + dfs(root, [], 0) + + if root["texts"]: + all_pathes.insert(0, root["texts"]) + if from_upstream.output_format in ["markdown", "text", "html"]: + cks = [] + for path in all_pathes: + txt = "" + for i in path: + txt += lines[i] + "\n" + cks.append(txt) + + self.set_output("chunks", [{"text": c} for c in cks if c]) + else: + cks = [] + images = [] + for path in all_pathes: + txt = "" + img = None + for i in path: + txt += lines[i] + "\n" + concat_img(img, id2image(section_images[i], partial(STORAGE_IMPL.get))) + cks.append(txt) + images.append(img) + + cks = [ + { + "text": RAGFlowPdfParser.remove_tag(c), + "image": img, + "positions": RAGFlowPdfParser.extract_positions(c), + } + for c, img in zip(cks, images) + ] + async with trio.open_nursery() as nursery: + for d in cks: + nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put), get_uuid()) + self.set_output("chunks", cks) + + self.callback(1, "Done.") diff --git a/rag/flow/hierarchical_merger/schema.py b/rag/flow/hierarchical_merger/schema.py new file mode 100644 index 000000000..a4d99f914 --- /dev/null +++ b/rag/flow/hierarchical_merger/schema.py @@ -0,0 +1,37 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class HierarchicalMergerFromUpstream(BaseModel): + created_time: float | None = Field(default=None, alias="_created_time") + elapsed_time: float | None = Field(default=None, alias="_elapsed_time") + + name: str + file: dict | None = Field(default=None) + chunks: list[dict[str, Any]] | None = Field(default=None) + + output_format: Literal["json", "chunks"] | None = Field(default=None) + json_result: list[dict[str, Any]] | None = Field(default=None, alias="json") + markdown_result: str | None = Field(default=None, alias="markdown") + text_result: str | None = Field(default=None, alias="text") + html_result: str | None = Field(default=None, alias="html") + + model_config = ConfigDict(populate_by_name=True, extra="forbid") + + # def to_dict(self, *, exclude_none: bool = True) -> dict: + # return self.model_dump(by_alias=True, exclude_none=exclude_none) diff --git a/rag/flow/parser/parser.py b/rag/flow/parser/parser.py index 8c1e8c1f0..f4378855d 100644 --- a/rag/flow/parser/parser.py +++ b/rag/flow/parser/parser.py @@ -13,20 +13,28 @@ # See the License for the specific language governing permissions and # limitations under the License. import io -import logging +import json +import os import random +from functools import partial import trio import numpy as np from PIL import Image from api.db import LLMType +from api.db.services.file2document_service import File2DocumentService +from api.db.services.file_service import FileService from api.db.services.llm_service import LLMBundle +from api.utils import get_uuid +from api.utils.base64_image import image2id from deepdoc.parser import ExcelParser from deepdoc.parser.pdf_parser import PlainParser, RAGFlowPdfParser, VisionParser +from rag.app.naive import Docx from rag.flow.base import ProcessBase, ProcessParamBase from rag.flow.parser.schema import ParserFromUpstream from rag.llm.cv_model import Base as VLM +from rag.utils.storage_factory import STORAGE_IMPL class ParserParam(ProcessParamBase): @@ -45,12 +53,14 @@ class ParserParam(ProcessParamBase): "word": [ "json", ], - "ppt": [], + "slides": [ + "json", + ], "image": [ "text" ], - "email": [], - "text": [ + "email": ["text", "json"], + "text&markdown": [ "text", "json" ], @@ -63,7 +73,6 @@ class ParserParam(ProcessParamBase): self.setups = { "pdf": { "parse_method": "deepdoc", # deepdoc/plain_text/vlm - "llm_id": "", "lang": "Chinese", "suffix": [ "pdf", @@ -85,23 +94,29 @@ class ParserParam(ProcessParamBase): ], "output_format": "json", }, - "markdown": { - "suffix": ["md", "markdown"], + "text&markdown": { + "suffix": ["md", "markdown", "mdx", "txt"], + "output_format": "json", + }, + "slides": { + "suffix": [ + "pptx", + ], "output_format": "json", }, - "ppt": {}, "image": { - "parse_method": ["ocr", "vlm"], + "parse_method": "ocr", "llm_id": "", "lang": "Chinese", + "system_prompt": "", "suffix": ["jpg", "jpeg", "png", "gif"], - "output_format": "json", + "output_format": "text", }, - "email": {}, - "text": { + "email": { "suffix": [ - "txt" + "eml", "msg" ], + "fields": ["from", "to", "cc", "bcc", "date", "subject", "body", "attachments", "metadata"], "output_format": "json", }, "audio": { @@ -131,13 +146,10 @@ class ParserParam(ProcessParamBase): pdf_config = self.setups.get("pdf", {}) if pdf_config: pdf_parse_method = pdf_config.get("parse_method", "") - self.check_valid_value(pdf_parse_method.lower(), "Parse method abnormal.", ["deepdoc", "plain_text", "vlm"]) + self.check_empty(pdf_parse_method, "Parse method abnormal.") - if pdf_parse_method not in ["deepdoc", "plain_text"]: - self.check_empty(pdf_config.get("llm_id"), "VLM") - - pdf_language = pdf_config.get("lang", "") - self.check_empty(pdf_language, "Language") + if pdf_parse_method.lower() not in ["deepdoc", "plain_text"]: + self.check_empty(pdf_config.get("lang", ""), "PDF VLM language") pdf_output_format = pdf_config.get("output_format", "") self.check_valid_value(pdf_output_format, "PDF output format abnormal.", self.allowed_output_format["pdf"]) @@ -147,32 +159,38 @@ class ParserParam(ProcessParamBase): spreadsheet_output_format = spreadsheet_config.get("output_format", "") self.check_valid_value(spreadsheet_output_format, "Spreadsheet output format abnormal.", self.allowed_output_format["spreadsheet"]) - doc_config = self.setups.get("doc", "") + doc_config = self.setups.get("word", "") if doc_config: doc_output_format = doc_config.get("output_format", "") - self.check_valid_value(doc_output_format, "Word processer document output format abnormal.", self.allowed_output_format["doc"]) + self.check_valid_value(doc_output_format, "Word processer document output format abnormal.", self.allowed_output_format["word"]) + + slides_config = self.setups.get("slides", "") + if slides_config: + slides_output_format = slides_config.get("output_format", "") + self.check_valid_value(slides_output_format, "Slides output format abnormal.", self.allowed_output_format["slides"]) image_config = self.setups.get("image", "") if image_config: image_parse_method = image_config.get("parse_method", "") - self.check_valid_value(image_parse_method.lower(), "Parse method abnormal.", ["ocr", "vlm"]) if image_parse_method not in ["ocr"]: - self.check_empty(image_config.get("llm_id"), "VLM") + self.check_empty(image_config.get("lang", ""), "Image VLM language") - image_language = image_config.get("lang", "") - self.check_empty(image_language, "Language") - - text_config = self.setups.get("text", "") + text_config = self.setups.get("text&markdown", "") if text_config: text_output_format = text_config.get("output_format", "") - self.check_valid_value(text_output_format, "Text output format abnormal.", self.allowed_output_format["text"]) + self.check_valid_value(text_output_format, "Text output format abnormal.", self.allowed_output_format["text&markdown"]) audio_config = self.setups.get("audio", "") if audio_config: - self.check_empty(audio_config.get("llm_id"), "VLM") + self.check_empty(audio_config.get("llm_id"), "Audio VLM") audio_language = audio_config.get("lang", "") self.check_empty(audio_language, "Language") + email_config = self.setups.get("email", "") + if email_config: + email_output_format = email_config.get("output_format", "") + self.check_valid_value(email_output_format, "Email output format abnormal.", self.allowed_output_format["email"]) + def get_input_form(self) -> dict[str, dict]: return {} @@ -180,21 +198,18 @@ class ParserParam(ProcessParamBase): class Parser(ProcessBase): component_name = "Parser" - def _pdf(self, from_upstream: ParserFromUpstream): + def _pdf(self, name, blob): self.callback(random.randint(1, 5) / 100.0, "Start to work on a PDF.") - - blob = from_upstream.blob conf = self._param.setups["pdf"] self.set_output("output_format", conf["output_format"]) - if conf.get("parse_method") == "deepdoc": + if conf.get("parse_method").lower() == "deepdoc": bboxes = RAGFlowPdfParser().parse_into_bboxes(blob, callback=self.callback) - elif conf.get("parse_method") == "plain_text": + elif conf.get("parse_method").lower() == "plain_text": lines, _ = PlainParser()(blob) bboxes = [{"text": t} for t, _ in lines] else: - assert conf.get("llm_id") - vision_model = LLMBundle(self._canvas._tenant_id, LLMType.IMAGE2TEXT, llm_name=conf.get("llm_id"), lang=self._param.setups["pdf"].get("lang")) + vision_model = LLMBundle(self._canvas._tenant_id, LLMType.IMAGE2TEXT, llm_name=conf.get("parse_method"), lang=self._param.setups["pdf"].get("lang")) lines, _ = VisionParser(vision_model=vision_model)(blob, callback=self.callback) bboxes = [] for t, poss in lines: @@ -214,66 +229,63 @@ class Parser(ProcessBase): mkdn += b.get("text", "") + "\n" self.set_output("markdown", mkdn) - def _spreadsheet(self, from_upstream: ParserFromUpstream): + def _spreadsheet(self, name, blob): self.callback(random.randint(1, 5) / 100.0, "Start to work on a Spreadsheet.") - - blob = from_upstream.blob conf = self._param.setups["spreadsheet"] self.set_output("output_format", conf["output_format"]) - - print("spreadsheet {conf=}", flush=True) spreadsheet_parser = ExcelParser() if conf.get("output_format") == "html": - html = spreadsheet_parser.html(blob, 1000000000) - self.set_output("html", html) + htmls = spreadsheet_parser.html(blob, 1000000000) + self.set_output("html", htmls[0]) elif conf.get("output_format") == "json": self.set_output("json", [{"text": txt} for txt in spreadsheet_parser(blob) if txt]) elif conf.get("output_format") == "markdown": self.set_output("markdown", spreadsheet_parser.markdown(blob)) - def _word(self, from_upstream: ParserFromUpstream): - from tika import parser as word_parser - + def _word(self, name, blob): self.callback(random.randint(1, 5) / 100.0, "Start to work on a Word Processor Document") - - blob = from_upstream.blob - name = from_upstream.name conf = self._param.setups["word"] self.set_output("output_format", conf["output_format"]) - - print("word {conf=}", flush=True) - doc_parsed = word_parser.from_buffer(blob) - - sections = [] - if doc_parsed.get("content"): - sections = doc_parsed["content"].split("\n") - sections = [{"text": section} for section in sections if section] - else: - logging.warning(f"tika.parser got empty content from {name}.") - + docx_parser = Docx() + sections, tbls = docx_parser(name, binary=blob) + sections = [{"text": section[0], "image": section[1]} for section in sections if section] + sections.extend([{"text": tb, "image": None} for ((_,tb), _) in tbls]) # json assert conf.get("output_format") == "json", "have to be json for doc" if conf.get("output_format") == "json": self.set_output("json", sections) - def _markdown(self, from_upstream: ParserFromUpstream): + def _slides(self, name, blob): + from deepdoc.parser.ppt_parser import RAGFlowPptParser as ppt_parser + + self.callback(random.randint(1, 5) / 100.0, "Start to work on a PowerPoint Document") + + conf = self._param.setups["slides"] + self.set_output("output_format", conf["output_format"]) + + ppt_parser = ppt_parser() + txts = ppt_parser(blob, 0, 100000, None) + + sections = [{"text": section} for section in txts if section.strip()] + + # json + assert conf.get("output_format") == "json", "have to be json for ppt" + if conf.get("output_format") == "json": + self.set_output("json", sections) + + def _markdown(self, name, blob): from functools import reduce from rag.app.naive import Markdown as naive_markdown_parser from rag.nlp import concat_img self.callback(random.randint(1, 5) / 100.0, "Start to work on a markdown.") - - blob = from_upstream.blob - name = from_upstream.name - conf = self._param.setups["markdown"] + conf = self._param.setups["text&markdown"] self.set_output("output_format", conf["output_format"]) markdown_parser = naive_markdown_parser() sections, tables = markdown_parser(name, blob, separate_tables=False) - # json - assert conf.get("output_format") == "json", "have to be json for doc" if conf.get("output_format") == "json": json_results = [] @@ -291,69 +303,51 @@ class Parser(ProcessBase): json_results.append(json_result) self.set_output("json", json_results) - - def _text(self, from_upstream: ParserFromUpstream): - from deepdoc.parser.utils import get_text - - self.callback(random.randint(1, 5) / 100.0, "Start to work on a text.") - - blob = from_upstream.blob - name = from_upstream.name - conf = self._param.setups["text"] - self.set_output("output_format", conf["output_format"]) - - # parse binary to text - text_content = get_text(name, binary=blob) - - if conf.get("output_format") == "json": - result = [{"text": text_content}] - self.set_output("json", result) else: - result = text_content - self.set_output("text", result) + self.set_output("text", "\n".join([section_text for section_text, _ in sections])) - def _image(self, from_upstream: ParserFromUpstream): + + def _image(self, name, blob): from deepdoc.vision import OCR self.callback(random.randint(1, 5) / 100.0, "Start to work on an image.") - - blob = from_upstream.blob conf = self._param.setups["image"] self.set_output("output_format", conf["output_format"]) img = Image.open(io.BytesIO(blob)).convert("RGB") - lang = conf["lang"] if conf["parse_method"] == "ocr": # use ocr, recognize chars only ocr = OCR() bxs = ocr(np.array(img)) # return boxes and recognize result txt = "\n".join([t[0] for _, t in bxs if t[0]]) - else: + lang = conf["lang"] # use VLM to describe the picture - cv_model = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT, llm_name=conf["llm_id"],lang=lang) + cv_model = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT, llm_name=conf["parse_method"], lang=lang) img_binary = io.BytesIO() img.save(img_binary, format="JPEG") img_binary.seek(0) - txt = cv_model.describe(img_binary.read()) + + system_prompt = conf.get("system_prompt") + if system_prompt: + txt = cv_model.describe_with_prompt(img_binary.read(), system_prompt) + else: + txt = cv_model.describe(img_binary.read()) self.set_output("text", txt) - def _audio(self, from_upstream: ParserFromUpstream): + def _audio(self, name, blob): import os import tempfile self.callback(random.randint(1, 5) / 100.0, "Start to work on an audio.") - blob = from_upstream.blob - name = from_upstream.name conf = self._param.setups["audio"] self.set_output("output_format", conf["output_format"]) lang = conf["lang"] _, ext = os.path.splitext(name) - tmp_path = "" with tempfile.NamedTemporaryFile(suffix=ext) as tmpf: tmpf.write(blob) tmpf.flush() @@ -364,15 +358,131 @@ class Parser(ProcessBase): self.set_output("text", txt) + def _email(self, name, blob): + self.callback(random.randint(1, 5) / 100.0, "Start to work on an email.") + + email_content = {} + conf = self._param.setups["email"] + target_fields = conf["fields"] + + _, ext = os.path.splitext(name) + if ext == ".eml": + # handle eml file + from email import policy + from email.parser import BytesParser + + msg = BytesParser(policy=policy.default).parse(io.BytesIO(blob)) + email_content['metadata'] = {} + # handle header info + for header, value in msg.items(): + # get fields like from, to, cc, bcc, date, subject + if header.lower() in target_fields: + email_content[header.lower()] = value + # get metadata + elif header.lower() not in ["from", "to", "cc", "bcc", "date", "subject"]: + email_content["metadata"][header.lower()] = value + # get body + if "body" in target_fields: + body_text, body_html = [], [] + def _add_content(m, content_type): + if content_type == "text/plain": + body_text.append( + m.get_payload(decode=True).decode(m.get_content_charset()) + ) + elif content_type == "text/html": + body_html.append( + m.get_payload(decode=True).decode(m.get_content_charset()) + ) + elif "multipart" in content_type: + if m.is_multipart(): + for part in m.iter_parts(): + _add_content(part, part.get_content_type()) + + _add_content(msg, msg.get_content_type()) + + email_content["text"] = body_text + email_content["text_html"] = body_html + # get attachment + if "attachments" in target_fields: + attachments = [] + for part in msg.iter_attachments(): + content_disposition = part.get("Content-Disposition") + if content_disposition: + dispositions = content_disposition.strip().split(";") + if dispositions[0].lower() == "attachment": + filename = part.get_filename() + payload = part.get_payload(decode=True) + attachments.append({ + "filename": filename, + "payload": payload, + }) + email_content["attachments"] = attachments + else: + # handle msg file + import extract_msg + print("handle a msg file.") + msg = extract_msg.Message(blob) + # handle header info + basic_content = { + "from": msg.sender, + "to": msg.to, + "cc": msg.cc, + "bcc": msg.bcc, + "date": msg.date, + "subject": msg.subject, + } + email_content.update({k: v for k, v in basic_content.items() if k in target_fields}) + # get metadata + email_content['metadata'] = { + 'message_id': msg.messageId, + 'in_reply_to': msg.inReplyTo, + } + # get body + if "body" in target_fields: + email_content["text"] = msg.body # usually empty. try text_html instead + email_content["text_html"] = msg.htmlBody + # get attachments + if "attachments" in target_fields: + attachments = [] + for t in msg.attachments: + attachments.append({ + "filename": t.name, + "payload": t.data # binary + }) + email_content["attachments"] = attachments + + if conf["output_format"] == "json": + self.set_output("json", [email_content]) + else: + content_txt = '' + for k, v in email_content.items(): + if isinstance(v, str): + # basic info + content_txt += f'{k}:{v}' + "\n" + elif isinstance(v, dict): + # metadata + content_txt += f'{k}:{json.dumps(v)}' + "\n" + elif isinstance(v, list): + # attachments or others + for fb in v: + if isinstance(fb, dict): + # attachments + content_txt += f'{fb["filename"]}:{fb["payload"]}' + "\n" + else: + # str, usually plain text + content_txt += fb + self.set_output("text", content_txt) + async def _invoke(self, **kwargs): function_map = { "pdf": self._pdf, - "markdown": self._markdown, + "text&markdown": self._markdown, "spreadsheet": self._spreadsheet, + "slides": self._slides, "word": self._word, - "text": self._text, "image": self._image, "audio": self._audio, + "email": self._email, } try: from_upstream = ParserFromUpstream.model_validate(kwargs) @@ -380,8 +490,25 @@ class Parser(ProcessBase): self.set_output("_ERROR", f"Input error: {str(e)}") return + name = from_upstream.name + if self._canvas._doc_id: + b, n = File2DocumentService.get_storage_address(doc_id=self._canvas._doc_id) + blob = STORAGE_IMPL.get(b, n) + else: + blob = FileService.get_blob(from_upstream.file["created_by"], from_upstream.file["id"]) + + done = False for p_type, conf in self._param.setups.items(): if from_upstream.name.split(".")[-1].lower() not in conf.get("suffix", []): continue - await trio.to_thread.run_sync(function_map[p_type], from_upstream) + await trio.to_thread.run_sync(function_map[p_type], name, blob) + done = True break + + if not done: + raise Exception("No suitable for file extension: `.%s`" % from_upstream.name.split(".")[-1].lower()) + + outs = self.output() + async with trio.open_nursery() as nursery: + for d in outs.get("json", []): + nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put), get_uuid()) diff --git a/rag/flow/parser/schema.py b/rag/flow/parser/schema.py index 37292e058..f43661762 100644 --- a/rag/flow/parser/schema.py +++ b/rag/flow/parser/schema.py @@ -20,6 +20,5 @@ class ParserFromUpstream(BaseModel): elapsed_time: float | None = Field(default=None, alias="_elapsed_time") name: str - blob: bytes - + file: dict | None = Field(default=None) model_config = ConfigDict(populate_by_name=True, extra="forbid") diff --git a/rag/flow/pipeline.py b/rag/flow/pipeline.py index 9f88d29ea..a1db65bbc 100644 --- a/rag/flow/pipeline.py +++ b/rag/flow/pipeline.py @@ -17,41 +17,92 @@ import datetime import json import logging import random -import time - +from timeit import default_timer as timer import trio - from agent.canvas import Graph from api.db.services.document_service import DocumentService +from api.db.services.task_service import has_canceled, TaskService, CANVAS_DEBUG_DOC_ID from rag.utils.redis_conn import REDIS_CONN class Pipeline(Graph): - def __init__(self, dsl: str, tenant_id=None, doc_id=None, task_id=None, flow_id=None): + def __init__(self, dsl: str|dict, tenant_id=None, doc_id=None, task_id=None, flow_id=None): + if isinstance(dsl, dict): + dsl = json.dumps(dsl, ensure_ascii=False) super().__init__(dsl, tenant_id, task_id) + if doc_id == CANVAS_DEBUG_DOC_ID: + doc_id = None self._doc_id = doc_id self._flow_id = flow_id self._kb_id = None - if doc_id: + if self._doc_id: self._kb_id = DocumentService.get_knowledgebase_id(doc_id) - assert self._kb_id, f"Can't find KB of this document: {doc_id}" + if not self._kb_id: + self._doc_id = None def callback(self, component_name: str, progress: float | int | None = None, message: str = "") -> None: + from rag.svr.task_executor import TaskCanceledException log_key = f"{self._flow_id}-{self.task_id}-logs" + timestamp = timer() + if has_canceled(self.task_id): + progress = -1 + message += "[CANCEL]" try: bin = REDIS_CONN.get(log_key) obj = json.loads(bin.encode("utf-8")) if obj: - if obj[-1]["component_name"] == component_name: - obj[-1]["trace"].append({"progress": progress, "message": message, "datetime": datetime.datetime.now().strftime("%H:%M:%S")}) + if obj[-1]["component_id"] == component_name: + obj[-1]["trace"].append( + { + "progress": progress, + "message": message, + "datetime": datetime.datetime.now().strftime("%H:%M:%S"), + "timestamp": timestamp, + "elapsed_time": timestamp - obj[-1]["trace"][-1]["timestamp"], + } + ) else: - obj.append({"component_name": component_name, "trace": [{"progress": progress, "message": message, "datetime": datetime.datetime.now().strftime("%H:%M:%S")}]}) + obj.append( + { + "component_id": component_name, + "trace": [{"progress": progress, "message": message, "datetime": datetime.datetime.now().strftime("%H:%M:%S"), "timestamp": timestamp, "elapsed_time": 0}], + } + ) else: - obj = [{"component_name": component_name, "trace": [{"progress": progress, "message": message, "datetime": datetime.datetime.now().strftime("%H:%M:%S")}]}] - REDIS_CONN.set_obj(log_key, obj, 60 * 10) + obj = [ + { + "component_id": component_name, + "trace": [{"progress": progress, "message": message, "datetime": datetime.datetime.now().strftime("%H:%M:%S"), "timestamp": timestamp, "elapsed_time": 0}], + } + ] + if component_name != "END" and self._doc_id and self.task_id: + percentage = 1.0 / len(self.components.items()) + finished = 0.0 + for o in obj: + for t in o["trace"]: + if t["progress"] < 0: + finished = -1 + break + if finished < 0: + break + finished += o["trace"][-1]["progress"] * percentage + + msg = "" + if len(obj[-1]["trace"]) == 1: + msg += f"\n-------------------------------------\n[{self.get_component_name(o['component_id'])}]:\n" + t = obj[-1]["trace"][-1] + msg += "%s: %s\n" % (t["datetime"], t["message"]) + TaskService.update_progress(self.task_id, {"progress": finished, "progress_msg": msg}) + elif component_name == "END" and not self._doc_id: + obj[-1]["trace"][-1]["dsl"] = json.loads(str(self)) + REDIS_CONN.set_obj(log_key, obj, 60 * 30) + except Exception as e: logging.exception(e) + if has_canceled(self.task_id): + raise TaskCanceledException(message) + def fetch_logs(self): log_key = f"{self._flow_id}-{self.task_id}-logs" try: @@ -62,34 +113,32 @@ class Pipeline(Graph): logging.exception(e) return [] - def reset(self): - super().reset() + + async def run(self, **kwargs): log_key = f"{self._flow_id}-{self.task_id}-logs" try: REDIS_CONN.set_obj(log_key, [], 60 * 10) except Exception as e: logging.exception(e) - - async def run(self, **kwargs): - st = time.perf_counter() + self.error = "" if not self.path: self.path.append("File") - - if self._doc_id: - DocumentService.update_by_id( - self._doc_id, {"progress": random.randint(0, 5) / 100.0, "progress_msg": "Start the pipeline...", "process_begin_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - ) - - self.error = "" - idx = len(self.path) - 1 - if idx == 0: cpn_obj = self.get_component_obj(self.path[0]) await cpn_obj.invoke(**kwargs) if cpn_obj.error(): self.error = "[ERROR]" + cpn_obj.error() - else: - idx += 1 - self.path.extend(cpn_obj.get_downstream()) + self.callback(cpn_obj.component_name, -1, self.error) + + if self._doc_id: + TaskService.update_progress(self.task_id, { + "progress": random.randint(0, 5) / 100.0, + "progress_msg": "Start the pipeline...", + "begin_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) + + idx = len(self.path) - 1 + cpn_obj = self.get_component_obj(self.path[idx]) + idx += 1 + self.path.extend(cpn_obj.get_downstream()) while idx < len(self.path) and not self.error: last_cpn = self.get_component_obj(self.path[idx - 1]) @@ -98,15 +147,28 @@ class Pipeline(Graph): async def invoke(): nonlocal last_cpn, cpn_obj await cpn_obj.invoke(**last_cpn.output()) + #if inspect.iscoroutinefunction(cpn_obj.invoke): + # await cpn_obj.invoke(**last_cpn.output()) + #else: + # cpn_obj.invoke(**last_cpn.output()) async with trio.open_nursery() as nursery: nursery.start_soon(invoke) + if cpn_obj.error(): self.error = "[ERROR]" + cpn_obj.error() - self.callback(cpn_obj.component_name, -1, self.error) + self.callback(cpn_obj._id, -1, self.error) break idx += 1 self.path.extend(cpn_obj.get_downstream()) - if self._doc_id: - DocumentService.update_by_id(self._doc_id, {"progress": 1 if not self.error else -1, "progress_msg": "Pipeline finished...\n" + self.error, "process_duration": time.perf_counter() - st}) + self.callback("END", 1 if not self.error else -1, json.dumps(self.get_component_obj(self.path[-1]).output(), ensure_ascii=False)) + + if not self.error: + return self.get_component_obj(self.path[-1]).output() + + TaskService.update_progress(self.task_id, { + "progress": -1, + "progress_msg": f"[ERROR]: {self.error}"}) + + return {} diff --git a/rag/flow/splitter/__init__.py b/rag/flow/splitter/__init__.py new file mode 100644 index 000000000..b4663378e --- /dev/null +++ b/rag/flow/splitter/__init__.py @@ -0,0 +1,15 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/rag/flow/chunker/schema.py b/rag/flow/splitter/schema.py similarity index 86% rename from rag/flow/chunker/schema.py rename to rag/flow/splitter/schema.py index bfeff447d..9144a4d9b 100644 --- a/rag/flow/chunker/schema.py +++ b/rag/flow/splitter/schema.py @@ -17,19 +17,20 @@ from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field -class ChunkerFromUpstream(BaseModel): +class SplitterFromUpstream(BaseModel): created_time: float | None = Field(default=None, alias="_created_time") elapsed_time: float | None = Field(default=None, alias="_elapsed_time") name: str - blob: bytes + file: dict | None = Field(default=None) + chunks: list[dict[str, Any]] | None = Field(default=None) output_format: Literal["json", "markdown", "text", "html"] | None = Field(default=None) json_result: list[dict[str, Any]] | None = Field(default=None, alias="json") markdown_result: str | None = Field(default=None, alias="markdown") text_result: str | None = Field(default=None, alias="text") - html_result: list[str] | None = Field(default=None, alias="html") + html_result: str | None = Field(default=None, alias="html") model_config = ConfigDict(populate_by_name=True, extra="forbid") diff --git a/rag/flow/splitter/splitter.py b/rag/flow/splitter/splitter.py new file mode 100644 index 000000000..9c6eb7bfd --- /dev/null +++ b/rag/flow/splitter/splitter.py @@ -0,0 +1,111 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random +from functools import partial + +import trio + +from api.utils import get_uuid +from api.utils.base64_image import id2image, image2id +from deepdoc.parser.pdf_parser import RAGFlowPdfParser +from rag.flow.base import ProcessBase, ProcessParamBase +from rag.flow.splitter.schema import SplitterFromUpstream +from rag.nlp import naive_merge, naive_merge_with_images +from rag.utils.storage_factory import STORAGE_IMPL + + +class SplitterParam(ProcessParamBase): + def __init__(self): + super().__init__() + self.chunk_token_size = 512 + self.delimiters = ["\n"] + self.overlapped_percent = 0 + + def check(self): + self.check_empty(self.delimiters, "Delimiters.") + self.check_positive_integer(self.chunk_token_size, "Chunk token size.") + self.check_decimal_float(self.overlapped_percent, "Overlapped percentage: [0, 1)") + + def get_input_form(self) -> dict[str, dict]: + return {} + + +class Splitter(ProcessBase): + component_name = "Splitter" + + async def _invoke(self, **kwargs): + try: + from_upstream = SplitterFromUpstream.model_validate(kwargs) + except Exception as e: + self.set_output("_ERROR", f"Input error: {str(e)}") + return + + deli = "" + for d in self._param.delimiters: + if len(d) > 1: + deli += f"`{d}`" + else: + deli += d + + self.set_output("output_format", "chunks") + self.callback(random.randint(1, 5) / 100.0, "Start to split into chunks.") + if from_upstream.output_format in ["markdown", "text", "html"]: + if from_upstream.output_format == "markdown": + payload = from_upstream.markdown_result + elif from_upstream.output_format == "text": + payload = from_upstream.text_result + else: # == "html" + payload = from_upstream.html_result + + if not payload: + payload = "" + + cks = naive_merge( + payload, + self._param.chunk_token_size, + deli, + self._param.overlapped_percent, + ) + self.set_output("chunks", [{"text": c.strip()} for c in cks if c.strip()]) + + self.callback(1, "Done.") + return + + # json + sections, section_images = [], [] + for o in from_upstream.json_result or []: + sections.append((o.get("text", ""), o.get("position_tag", ""))) + section_images.append(id2image(o.get("img_id"), partial(STORAGE_IMPL.get))) + + chunks, images = naive_merge_with_images( + sections, + section_images, + self._param.chunk_token_size, + deli, + self._param.overlapped_percent, + ) + cks = [ + { + "text": RAGFlowPdfParser.remove_tag(c), + "image": img, + "positions": [[pos[0][-1]+1, *pos[1:]] for pos in RAGFlowPdfParser.extract_positions(c)], + } + for c, img in zip(chunks, images) if c.strip() + ] + async with trio.open_nursery() as nursery: + for d in cks: + nursery.start_soon(image2id, d, partial(STORAGE_IMPL.put), get_uuid()) + self.set_output("chunks", cks) + self.callback(1, "Done.") diff --git a/rag/flow/tests/client.py b/rag/flow/tests/client.py index cf4a4db37..375a66d4f 100644 --- a/rag/flow/tests/client.py +++ b/rag/flow/tests/client.py @@ -30,7 +30,7 @@ def print_logs(pipeline: Pipeline): while True: time.sleep(5) logs = pipeline.fetch_logs() - logs_str = json.dumps(logs) + logs_str = json.dumps(logs, ensure_ascii=False) if logs_str != last_logs: print(logs_str) last_logs = logs_str diff --git a/rag/flow/tests/dsl_examples/general_pdf_all.json b/rag/flow/tests/dsl_examples/general_pdf_all.json index 352dc847d..40f796af6 100644 --- a/rag/flow/tests/dsl_examples/general_pdf_all.json +++ b/rag/flow/tests/dsl_examples/general_pdf_all.json @@ -38,6 +38,13 @@ ], "output_format": "json" }, + "slides": { + "parse_method": "presentation", + "suffix": [ + "pptx" + ], + "output_format": "json" + }, "markdown": { "suffix": [ "md", @@ -82,19 +89,36 @@ "lang": "Chinese", "llm_id": "SenseVoiceSmall", "output_format": "json" + }, + "email": { + "suffix": [ + "msg" + ], + "fields": [ + "from", + "to", + "cc", + "bcc", + "date", + "subject", + "body", + "attachments" + ], + "output_format": "json" } } } }, - "downstream": ["Chunker:0"], + "downstream": ["Splitter:0"], "upstream": ["Begin"] }, - "Chunker:0": { + "Splitter:0": { "obj": { - "component_name": "Chunker", + "component_name": "Splitter", "params": { - "method": "general", - "auto_keywords": 5 + "chunk_token_size": 512, + "delimiters": ["\n"], + "overlapped_percent": 0 } }, "downstream": ["Tokenizer:0"], diff --git a/rag/flow/tests/dsl_examples/hierarchical_merger.json b/rag/flow/tests/dsl_examples/hierarchical_merger.json new file mode 100644 index 000000000..98df8a937 --- /dev/null +++ b/rag/flow/tests/dsl_examples/hierarchical_merger.json @@ -0,0 +1,84 @@ +{ + "components": { + "File": { + "obj":{ + "component_name": "File", + "params": { + } + }, + "downstream": ["Parser:0"], + "upstream": [] + }, + "Parser:0": { + "obj": { + "component_name": "Parser", + "params": { + "setups": { + "pdf": { + "parse_method": "deepdoc", + "vlm_name": "", + "lang": "Chinese", + "suffix": [ + "pdf" + ], + "output_format": "json" + }, + "spreadsheet": { + "suffix": [ + "xls", + "xlsx", + "csv" + ], + "output_format": "html" + }, + "word": { + "suffix": [ + "doc", + "docx" + ], + "output_format": "json" + }, + "markdown": { + "suffix": [ + "md", + "markdown" + ], + "output_format": "text" + }, + "text": { + "suffix": ["txt"], + "output_format": "json" + } + } + } + }, + "downstream": ["Splitter:0"], + "upstream": ["File"] + }, + "Splitter:0": { + "obj": { + "component_name": "Splitter", + "params": { + "chunk_token_size": 512, + "delimiters": ["\r\n"], + "overlapped_percent": 0 + } + }, + "downstream": ["HierarchicalMerger:0"], + "upstream": ["Parser:0"] + }, + "HierarchicalMerger:0": { + "obj": { + "component_name": "HierarchicalMerger", + "params": { + "levels": [["^#[^#]"], ["^##[^#]"], ["^###[^#]"], ["^####[^#]"]], + "hierarchy": 2 + } + }, + "downstream": [], + "upstream": ["Splitter:0"] + } + }, + "path": [] +} + diff --git a/rag/flow/tokenizer/schema.py b/rag/flow/tokenizer/schema.py index d58725171..e74a5825f 100644 --- a/rag/flow/tokenizer/schema.py +++ b/rag/flow/tokenizer/schema.py @@ -22,16 +22,16 @@ class TokenizerFromUpstream(BaseModel): elapsed_time: float | None = Field(default=None, alias="_elapsed_time") name: str = "" - blob: bytes + file: dict | None = Field(default=None) - output_format: Literal["json", "markdown", "text", "html"] | None = Field(default=None) + output_format: Literal["json", "markdown", "text", "html", "chunks"] | None = Field(default=None) chunks: list[dict[str, Any]] | None = Field(default=None) json_result: list[dict[str, Any]] | None = Field(default=None, alias="json") markdown_result: str | None = Field(default=None, alias="markdown") text_result: str | None = Field(default=None, alias="text") - html_result: list[str] | None = Field(default=None, alias="html") + html_result: str | None = Field(default=None, alias="html") model_config = ConfigDict(populate_by_name=True, extra="forbid") @@ -40,12 +40,14 @@ class TokenizerFromUpstream(BaseModel): if self.chunks: return self - if self.output_format in {"markdown", "text"}: + if self.output_format in {"markdown", "text", "html"}: if self.output_format == "markdown" and not self.markdown_result: raise ValueError("output_format=markdown requires a markdown payload (field: 'markdown' or 'markdown_result').") if self.output_format == "text" and not self.text_result: raise ValueError("output_format=text requires a text payload (field: 'text' or 'text_result').") + if self.output_format == "html" and not self.html_result: + raise ValueError("output_format=text requires a html payload (field: 'html' or 'html_result').") else: - if not self.json_result: + if not self.json_result and not self.chunks: raise ValueError("When no chunks are provided and output_format is not markdown/text, a JSON list payload is required (field: 'json' or 'json_result').") return self diff --git a/rag/flow/tokenizer/tokenizer.py b/rag/flow/tokenizer/tokenizer.py index 5b43a9d82..425e20397 100644 --- a/rag/flow/tokenizer/tokenizer.py +++ b/rag/flow/tokenizer/tokenizer.py @@ -37,6 +37,7 @@ class TokenizerParam(ProcessParamBase): super().__init__() self.search_method = ["full_text", "embedding"] self.filename_embd_weight = 0.1 + self.fields = ["text"] def check(self): for v in self.search_method: @@ -61,10 +62,14 @@ class Tokenizer(ProcessBase): embedding_model = LLMBundle(self._canvas._tenant_id, LLMType.EMBEDDING, llm_name=embedding_id) texts = [] for c in chunks: - if c.get("questions"): - texts.append("\n".join(c["questions"])) - else: - texts.append(re.sub(r"]{0,12})?>", " ", c["text"])) + txt = "" + for f in self._param.fields: + f = c.get(f) + if isinstance(f, str): + txt += f + elif isinstance(f, list): + txt += "\n".join(f) + texts.append(re.sub(r"]{0,12})?>", " ", txt)) vts, c = embedding_model.encode([name]) token_count += c tts = np.concatenate([vts[0] for _ in range(len(texts))], axis=0) @@ -103,26 +108,36 @@ class Tokenizer(ProcessBase): self.set_output("_ERROR", f"Input error: {str(e)}") return + self.set_output("output_format", "chunks") parts = sum(["full_text" in self._param.search_method, "embedding" in self._param.search_method]) if "full_text" in self._param.search_method: self.callback(random.randint(1, 5) / 100.0, "Start to tokenize.") if from_upstream.chunks: chunks = from_upstream.chunks for i, ck in enumerate(chunks): + ck["title_tks"] = rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", from_upstream.name)) + ck["title_sm_tks"] = rag_tokenizer.fine_grained_tokenize(ck["title_tks"]) if ck.get("questions"): - ck["question_tks"] = rag_tokenizer.tokenize("\n".join(ck["questions"])) + ck["question_kwd"] = ck["questions"].split("\n") + ck["question_tks"] = rag_tokenizer.tokenize(str(ck["questions"])) if ck.get("keywords"): - ck["important_tks"] = rag_tokenizer.tokenize("\n".join(ck["keywords"])) - ck["content_ltks"] = rag_tokenizer.tokenize(ck["text"]) - ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) + ck["important_kwd"] = ck["keywords"].split(",") + ck["important_tks"] = rag_tokenizer.tokenize(str(ck["keywords"])) + if ck.get("summary"): + ck["content_ltks"] = rag_tokenizer.tokenize(str(ck["summary"])) + ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) + else: + ck["content_ltks"] = rag_tokenizer.tokenize(ck["text"]) + ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) if i % 100 == 99: self.callback(i * 1.0 / len(chunks) / parts) + elif from_upstream.output_format in ["markdown", "text", "html"]: if from_upstream.output_format == "markdown": payload = from_upstream.markdown_result elif from_upstream.output_format == "text": payload = from_upstream.text_result - else: # == "html" + else: payload = from_upstream.html_result if not payload: @@ -130,12 +145,16 @@ class Tokenizer(ProcessBase): ck = {"text": payload} if "full_text" in self._param.search_method: - ck["content_ltks"] = rag_tokenizer.tokenize(kwargs.get(kwargs["output_format"], "")) + ck["title_tks"] = rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", from_upstream.name)) + ck["title_sm_tks"] = rag_tokenizer.fine_grained_tokenize(ck["title_tks"]) + ck["content_ltks"] = rag_tokenizer.tokenize(payload) ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) chunks = [ck] else: chunks = from_upstream.json_result for i, ck in enumerate(chunks): + ck["title_tks"] = rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", from_upstream.name)) + ck["title_sm_tks"] = rag_tokenizer.fine_grained_tokenize(ck["title_tks"]) ck["content_ltks"] = rag_tokenizer.tokenize(ck["text"]) ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) if i % 100 == 99: diff --git a/rag/llm/embedding_model.py b/rag/llm/embedding_model.py index 703223c77..7d5494b4f 100644 --- a/rag/llm/embedding_model.py +++ b/rag/llm/embedding_model.py @@ -33,7 +33,7 @@ from zhipuai import ZhipuAI from api import settings from api.utils.file_utils import get_home_cache_dir from api.utils.log_utils import log_exception -from rag.utils import num_tokens_from_string, truncate, total_token_count_from_response +from rag.utils import num_tokens_from_string, truncate class Base(ABC): @@ -52,7 +52,15 @@ class Base(ABC): raise NotImplementedError("Please implement encode method!") def total_token_count(self, resp): - return total_token_count_from_response(resp) + try: + return resp.usage.total_tokens + except Exception: + pass + try: + return resp["usage"]["total_tokens"] + except Exception: + pass + return 0 class DefaultEmbedding(Base): @@ -138,7 +146,7 @@ class OpenAIEmbed(Base): ress = [] total_tokens = 0 for i in range(0, len(texts), batch_size): - res = self.client.embeddings.create(input=texts[i : i + batch_size], model=self.model_name, encoding_format="float") + res = self.client.embeddings.create(input=texts[i : i + batch_size], model=self.model_name, encoding_format="float", extra_body={"drop_params": True}) try: ress.extend([d.embedding for d in res.data]) total_tokens += self.total_token_count(res) @@ -147,7 +155,7 @@ class OpenAIEmbed(Base): return np.array(ress), total_tokens def encode_queries(self, text): - res = self.client.embeddings.create(input=[truncate(text, 8191)], model=self.model_name, encoding_format="float") + res = self.client.embeddings.create(input=[truncate(text, 8191)], model=self.model_name, encoding_format="float",extra_body={"drop_params": True}) return np.array(res.data[0].embedding), self.total_token_count(res) @@ -489,7 +497,6 @@ class MistralEmbed(Base): def encode_queries(self, text): import time import random - retry_max = 5 while retry_max > 0: try: @@ -748,7 +755,7 @@ class SILICONFLOWEmbed(Base): texts_batch = texts[i : i + batch_size] if self.model_name in ["BAAI/bge-large-zh-v1.5", "BAAI/bge-large-en-v1.5"]: # limit 512, 340 is almost safe - texts_batch = [" " if not text.strip() else truncate(text, 340) for text in texts_batch] + texts_batch = [" " if not text.strip() else truncate(text, 256) for text in texts_batch] else: texts_batch = [" " if not text.strip() else text for text in texts_batch] @@ -937,7 +944,6 @@ class GiteeEmbed(SILICONFLOWEmbed): base_url = "https://ai.gitee.com/v1/embeddings" super().__init__(key, model_name, base_url) - class DeepInfraEmbed(OpenAIEmbed): _FACTORY_NAME = "DeepInfra" diff --git a/rag/nlp/__init__.py b/rag/nlp/__init__.py index 8208fba85..736298679 100644 --- a/rag/nlp/__init__.py +++ b/rag/nlp/__init__.py @@ -292,6 +292,7 @@ def tokenize_chunks(chunks, doc, eng, pdf_parser=None): res.append(d) return res + def tokenize_chunks_with_images(chunks, doc, eng, images): res = [] # wrap up as es documents @@ -306,6 +307,7 @@ def tokenize_chunks_with_images(chunks, doc, eng, images): res.append(d) return res + def tokenize_table(tbls, doc, eng, batch_size=10): res = [] # add tables @@ -579,7 +581,9 @@ def naive_merge(sections: str | list, chunk_token_num=128, delimiter="\n。; from deepdoc.parser.pdf_parser import RAGFlowPdfParser if not sections: return [] - if isinstance(sections[0], type("")): + if isinstance(sections, str): + sections = [sections] + if isinstance(sections[0], str): sections = [(s, "") for s in sections] cks = [""] tk_nums = [0] diff --git a/rag/nlp/search.py b/rag/nlp/search.py index b1617b9a7..db1423095 100644 --- a/rag/nlp/search.py +++ b/rag/nlp/search.py @@ -383,7 +383,7 @@ class Dealer: vector_column = f"q_{dim}_vec" zero_vector = [0.0] * dim sim_np = np.array(sim) - filtered_count = (sim_np >= similarity_threshold).sum() + filtered_count = (sim_np >= similarity_threshold).sum() ranks["total"] = int(filtered_count) # Convert from np.int64 to Python int otherwise JSON serializable error for i in idx: if sim[i] < similarity_threshold: @@ -444,12 +444,27 @@ class Dealer: def chunk_list(self, doc_id: str, tenant_id: str, kb_ids: list[str], max_count=1024, offset=0, - fields=["docnm_kwd", "content_with_weight", "img_id"]): + fields=["docnm_kwd", "content_with_weight", "img_id"], + sort_by_position: bool = False): condition = {"doc_id": doc_id} + + fields_set = set(fields or []) + if sort_by_position: + for need in ("page_num_int", "position_int", "top_int"): + if need not in fields_set: + fields_set.add(need) + fields = list(fields_set) + + orderBy = OrderByExpr() + if sort_by_position: + orderBy.asc("page_num_int") + orderBy.asc("position_int") + orderBy.asc("top_int") + res = [] bs = 128 for p in range(offset, max_count, bs): - es_res = self.dataStore.search(fields, [], condition, [], OrderByExpr(), p, bs, index_name(tenant_id), + es_res = self.dataStore.search(fields, [], condition, [], orderBy, p, bs, index_name(tenant_id), kb_ids) dict_chunks = self.dataStore.getFields(es_res, fields) for id, doc in dict_chunks.items(): diff --git a/rag/prompts/generator.py b/rag/prompts/generator.py index 8291b4e21..89c9c5c1e 100644 --- a/rag/prompts/generator.py +++ b/rag/prompts/generator.py @@ -436,4 +436,217 @@ def gen_meta_filter(chat_mdl, meta_data:dict, query: str) -> list: return ans except Exception: logging.exception(f"Loading json failure: {ans}") - return [] \ No newline at end of file + return [] + + +def gen_json(system_prompt:str, user_prompt:str, chat_mdl): + _, msg = message_fit_in(form_message(system_prompt, user_prompt), chat_mdl.max_length) + ans = chat_mdl.chat(msg[0]["content"], msg[1:]) + ans = re.sub(r"(^.*|```json\n|```\n*$)", "", ans, flags=re.DOTALL) + try: + return json_repair.loads(ans) + except Exception: + logging.exception(f"Loading json failure: {ans}") + + +TOC_DETECTION = load_prompt("toc_detection") +def detect_table_of_contents(page_1024:list[str], chat_mdl): + toc_secs = [] + for i, sec in enumerate(page_1024[:22]): + ans = gen_json(PROMPT_JINJA_ENV.from_string(TOC_DETECTION).render(page_txt=sec), "Only JSON please.", chat_mdl) + if toc_secs and not ans["exists"]: + break + toc_secs.append(sec) + return toc_secs + + +TOC_EXTRACTION = load_prompt("toc_extraction") +TOC_EXTRACTION_CONTINUE = load_prompt("toc_extraction_continue") +def extract_table_of_contents(toc_pages, chat_mdl): + if not toc_pages: + return [] + + return gen_json(PROMPT_JINJA_ENV.from_string(TOC_EXTRACTION).render(toc_page="\n".join(toc_pages)), "Only JSON please.", chat_mdl) + + +def toc_index_extractor(toc:list[dict], content:str, chat_mdl): + tob_extractor_prompt = """ + You are given a table of contents in a json format and several pages of a document, your job is to add the physical_index to the table of contents in the json format. + + The provided pages contains tags like and to indicate the physical location of the page X. + + The structure variable is the numeric system which represents the index of the hierarchy section in the table of contents. For example, the first section has structure index 1, the first subsection has structure index 1.1, the second subsection has structure index 1.2, etc. + + The response should be in the following JSON format: + [ + { + "structure": (string), + "title": , + "physical_index": "<physical_index_X>" (keep the format) + }, + ... + ] + + Only add the physical_index to the sections that are in the provided pages. + If the title of the section are not in the provided pages, do not add the physical_index to it. + Directly return the final JSON structure. Do not output anything else.""" + + prompt = tob_extractor_prompt + '\nTable of contents:\n' + json.dumps(toc, ensure_ascii=False, indent=2) + '\nDocument pages:\n' + content + return gen_json(prompt, "Only JSON please.", chat_mdl) + + +TOC_INDEX = load_prompt("toc_index") +def table_of_contents_index(toc_arr: list[dict], sections: list[str], chat_mdl): + if not toc_arr or not sections: + return [] + + toc_map = {} + for i, it in enumerate(toc_arr): + k1 = (it["structure"]+it["title"]).replace(" ", "") + k2 = it["title"].strip() + if k1 not in toc_map: + toc_map[k1] = [] + if k2 not in toc_map: + toc_map[k2] = [] + toc_map[k1].append(i) + toc_map[k2].append(i) + + for it in toc_arr: + it["indices"] = [] + for i, sec in enumerate(sections): + sec = sec.strip() + if sec.replace(" ", "") in toc_map: + for j in toc_map[sec.replace(" ", "")]: + toc_arr[j]["indices"].append(i) + + all_pathes = [] + def dfs(start, path): + nonlocal all_pathes + if start >= len(toc_arr): + if path: + all_pathes.append(path) + return + if not toc_arr[start]["indices"]: + dfs(start+1, path) + return + added = False + for j in toc_arr[start]["indices"]: + if path and j < path[-1][0]: + continue + _path = deepcopy(path) + _path.append((j, start)) + added = True + dfs(start+1, _path) + if not added and path: + all_pathes.append(path) + + dfs(0, []) + path = max(all_pathes, key=lambda x:len(x)) + for it in toc_arr: + it["indices"] = [] + for j, i in path: + toc_arr[i]["indices"] = [j] + print(json.dumps(toc_arr, ensure_ascii=False, indent=2)) + + i = 0 + while i < len(toc_arr): + it = toc_arr[i] + if it["indices"]: + i += 1 + continue + + if i>0 and toc_arr[i-1]["indices"]: + st_i = toc_arr[i-1]["indices"][-1] + else: + st_i = 0 + e = i + 1 + while e <len(toc_arr) and not toc_arr[e]["indices"]: + e += 1 + if e >= len(toc_arr): + e = len(sections) + else: + e = toc_arr[e]["indices"][0] + + for j in range(st_i, min(e+1, len(sections))): + ans = gen_json(PROMPT_JINJA_ENV.from_string(TOC_INDEX).render( + structure=it["structure"], + title=it["title"], + text=sections[j]), "Only JSON please.", chat_mdl) + if ans["exist"] == "yes": + it["indices"].append(j) + break + + i += 1 + + return toc_arr + + +def check_if_toc_transformation_is_complete(content, toc, chat_mdl): + prompt = """ + You are given a raw table of contents and a table of contents. + Your job is to check if the table of contents is complete. + + Reply format: + {{ + "thinking": <why do you think the cleaned table of contents is complete or not> + "completed": "yes" or "no" + }} + Directly return the final JSON structure. Do not output anything else.""" + + prompt = prompt + '\n Raw Table of contents:\n' + content + '\n Cleaned Table of contents:\n' + toc + response = gen_json(prompt, "Only JSON please.", chat_mdl) + return response['completed'] + + +def toc_transformer(toc_pages, chat_mdl): + init_prompt = """ + You are given a table of contents, You job is to transform the whole table of content into a JSON format included table_of_contents. + + The `structure` is the numeric system which represents the index of the hierarchy section in the table of contents. For example, the first section has structure index 1, the first subsection has structure index 1.1, the second subsection has structure index 1.2, etc. + The `title` is a short phrase or a several-words term. + + The response should be in the following JSON format: + [ + { + "structure": <structure index, "x.x.x" or None> (string), + "title": <title of the section> + }, + ... + ], + You should transform the full table of contents in one go. + Directly return the final JSON structure, do not output anything else. """ + + toc_content = "\n".join(toc_pages) + prompt = init_prompt + '\n Given table of contents\n:' + toc_content + def clean_toc(arr): + for a in arr: + a["title"] = re.sub(r"[.·….]{2,}", "", a["title"]) + last_complete = gen_json(prompt, "Only JSON please.", chat_mdl) + if_complete = check_if_toc_transformation_is_complete(toc_content, json.dumps(last_complete, ensure_ascii=False, indent=2), chat_mdl) + clean_toc(last_complete) + if if_complete == "yes": + return last_complete + + while not (if_complete == "yes"): + prompt = f""" + Your task is to continue the table of contents json structure, directly output the remaining part of the json structure. + The response should be in the following JSON format: + + The raw table of contents json structure is: + {toc_content} + + The incomplete transformed table of contents json structure is: + {json.dumps(last_complete[-24:], ensure_ascii=False, indent=2)} + + Please continue the json structure, directly output the remaining part of the json structure.""" + new_complete = gen_json(prompt, "Only JSON please.", chat_mdl) + if not new_complete or str(last_complete).find(str(new_complete)) >= 0: + break + clean_toc(new_complete) + last_complete.extend(new_complete) + if_complete = check_if_toc_transformation_is_complete(toc_content, json.dumps(last_complete, ensure_ascii=False, indent=2), chat_mdl) + + return last_complete + + + diff --git a/rag/prompts/toc_detection.md b/rag/prompts/toc_detection.md new file mode 100644 index 000000000..29e068a7a --- /dev/null +++ b/rag/prompts/toc_detection.md @@ -0,0 +1,29 @@ +You are an AI assistant designed to analyze text content and detect whether a table of contents (TOC) list exists on the given page. Follow these steps: + +1. **Analyze the Input**: Carefully review the provided text content. +2. **Identify Key Features**: Look for common indicators of a TOC, such as: + - Section titles or headings paired with page numbers. + - Patterns like repeated formatting (e.g., bold/italicized text, dots/dashes between titles and numbers). + - Phrases like "Table of Contents," "Contents," or similar headings. + - Logical grouping of topics/subtopics with sequential page references. +3. **Discern Negative Features**: + - The text contains no numbers, or the numbers present are clearly not page references (e.g., dates, statistical figures, phone numbers, version numbers). + - The text consists of full, descriptive sentences and paragraphs that form a narrative, present arguments, or explain concepts, rather than succinctly listing topics. + - Contains citations with authors, publication years, journal titles, and page ranges (e.g., "Smith, J. (2020). Journal Title, 10(2), 45-67."). + - Lists keywords or terms followed by multiple page numbers, often in alphabetical order. + - Comprises terms followed by their definitions or explanations. + - Labeled with headers like "Appendix A," "Appendix B," etc. + - Contains expressive language thanking individuals or organizations for their support or contributions. +4. **Evaluate Evidence**: Weigh the presence/absence of these features to determine if the content resembles a TOC. +5. **Output Format**: Provide your response in the following JSON structure: + ```json + { + "reasoning": "Step-by-step explanation of your analysis based on the features identified." , + "exists": true/false + } + ``` +6. **DO NOT** output anything else except JSON structure. + +**Input text Content ( Text-Only Extraction ):** +{{ page_txt }} + diff --git a/rag/prompts/toc_extraction.md b/rag/prompts/toc_extraction.md new file mode 100644 index 000000000..02e1d031f --- /dev/null +++ b/rag/prompts/toc_extraction.md @@ -0,0 +1,53 @@ +You are an expert parser and data formatter. Your task is to analyze the provided table of contents (TOC) text and convert it into a valid JSON array of objects. + +**Instructions:** +1. Analyze each line of the input TOC. +2. For each line, extract the following three pieces of information: + * `structure`: The hierarchical index/numbering (e.g., "1", "2.1", "3.2.5", "A.1"). If a line has no visible numbering or structure indicator (like a main "Chapter" title), use `null`. + * `title`: The textual title of the section or chapter. This should be the main descriptive text, clean and without the page number. +3. Output **only** a valid JSON array. Do not include any other text, explanations, or markdown code block fences (like ```json) in your response. + +**JSON Format:** +The output must be a list of objects following this exact schema: +```json +[ + { + "structure": <structure index, "x.x.x" or None> (string), + "title": <title of the section> + }, + ... +] +``` + +**Input Example:** +``` +Contents +1 Introduction to the System ... 1 +1.1 Overview .... 2 +1.2 Key Features .... 5 +2 Installation Guide ....8 +2.1 Prerequisites ........ 9 +2.2 Step-by-Step Process ........ 12 +Appendix A: Specifications ..... 45 +References ... 47 +``` + +**Expected Output For The Example:** +```json +[ + {"structure": null, "title": "Contents"}, + {"structure": "1", "title": "Introduction to the System"}, + {"structure": "1.1", "title": "Overview"}, + {"structure": "1.2", "title": "Key Features"}, + {"structure": "2", "title": "Installation Guide"}, + {"structure": "2.1", "title": "Prerequisites"}, + {"structure": "2.2", "title": "Step-by-Step Process"}, + {"structure": "A", "title": "Specifications"}, + {"structure": null, "title": "References"} +] +``` + +**Now, process the following TOC input:** +``` +{{ toc_page }} +``` \ No newline at end of file diff --git a/rag/prompts/toc_extraction_continue.md b/rag/prompts/toc_extraction_continue.md new file mode 100644 index 000000000..433ac68ad --- /dev/null +++ b/rag/prompts/toc_extraction_continue.md @@ -0,0 +1,60 @@ +You are an expert parser and data formatter, currently in the process of building a JSON array from a multi-page table of contents (TOC). Your task is to analyze the new page of content and **append** the new entries to the existing JSON array. + +**Instructions:** +1. You will be given two inputs: + * `current_page_text`: The text content from the new page of the TOC. + * `existing_json`: The valid JSON array you have generated from the previous pages. +2. Analyze each line of the `current_page_text` input. +3. For each new line, extract the following three pieces of information: + * `structure`: The hierarchical index/numbering (e.g., "1", "2.1", "3.2.5"). Use `null` if none exists. + * `title`: The clean textual title of the section or chapter. + * `page`: The page number on which the section starts. Extract only the number. Use `null` if not present. +4. **Append these new entries** to the `existing_json` array. Do not modify, reorder, or delete any of the existing entries. +5. Output **only** the complete, updated JSON array. Do not include any other text, explanations, or markdown code block fences (like ```json). + +**JSON Format:** +The output must be a valid JSON array following this schema: +```json +[ + { + "structure": <string or null>, + "title": <string>, + "page": <number or null> + }, + ... +] +``` + +**Input Example:** +`current_page_text`: +``` +3.2 Advanced Configuration ........... 25 +3.3 Troubleshooting .................. 28 +4 User Management .................... 30 +``` + +`existing_json`: +```json +[ + {"structure": "1", "title": "Introduction", "page": 1}, + {"structure": "2", "title": "Installation", "page": 5}, + {"structure": "3", "title": "Configuration", "page": 12}, + {"structure": "3.1", "title": "Basic Setup", "page": 15} +] +``` + +**Expected Output For The Example:** +```json +[ + {"structure": "3.2", "title": "Advanced Configuration", "page": 25}, + {"structure": "3.3", "title": "Troubleshooting", "page": 28}, + {"structure": "4", "title": "User Management", "page": 30} +] +``` + +**Now, process the following inputs:** +`current_page_text`: +{{ toc_page }} + +`existing_json`: +{{ toc_json }} \ No newline at end of file diff --git a/rag/prompts/toc_index.md b/rag/prompts/toc_index.md new file mode 100644 index 000000000..860356d50 --- /dev/null +++ b/rag/prompts/toc_index.md @@ -0,0 +1,20 @@ +You are an expert analyst tasked with matching text content to the title. + +**Instructions:** +1. Analyze the given title with its numeric structure index and the provided text. +2. Determine whether the title is mentioned as a section tile in the given text. +3. Provide a concise, step-by-step reasoning for your decision. +4. Output **only** the complete JSON object. Do not include any other text, explanations, or markdown code block fences (like ```json). + +**Output Format:** +Your output must be a valid JSON object with the following keys: +{ +"reasoning": "Step-by-step explanation of your analysis.", +"exist": "<yes or no>", +} + +** The title: ** +{{ structure }} {{ title }} + +** Given text: ** +{{ text }} \ No newline at end of file diff --git a/rag/svr/task_executor.py b/rag/svr/task_executor.py index ff89d2e55..eb03a56b8 100644 --- a/rag/svr/task_executor.py +++ b/rag/svr/task_executor.py @@ -21,14 +21,18 @@ import sys import threading import time -from api.utils import get_uuid +import json_repair + +from api.db.services.canvas_service import UserCanvasService +from api.db.services.knowledgebase_service import KnowledgebaseService +from api.db.services.pipeline_operation_log_service import PipelineOperationLogService from api.utils.api_utils import timeout +from api.utils.base64_image import image2id from api.utils.log_utils import init_root_logger, get_project_base_directory -from graphrag.general.index import run_graphrag +from graphrag.general.index import run_graphrag_for_kb from graphrag.utils import get_llm_cache, set_llm_cache, get_tags_from_cache, set_tags_to_cache from rag.flow.pipeline import Pipeline from rag.prompts.generator import keyword_extraction, question_proposal, content_tagging - import logging import os from datetime import datetime @@ -37,7 +41,6 @@ import xxhash import copy import re from functools import partial -from io import BytesIO from multiprocessing.context import TimeoutError from timeit import default_timer as timer import tracemalloc @@ -45,21 +48,19 @@ import signal import trio import exceptiongroup import faulthandler - import numpy as np from peewee import DoesNotExist - -from api.db import LLMType, ParserType +from api.db import LLMType, ParserType, PipelineTaskType from api.db.services.document_service import DocumentService from api.db.services.llm_service import LLMBundle -from api.db.services.task_service import TaskService, has_canceled +from api.db.services.task_service import TaskService, has_canceled, CANVAS_DEBUG_DOC_ID, GRAPH_RAPTOR_FAKE_DOC_ID from api.db.services.file2document_service import File2DocumentService from api import settings from api.versions import get_ragflow_version from api.db.db_models import close_connection from rag.app import laws, paper, presentation, manual, qa, table, book, resume, picture, naive, one, audio, \ email, tag -from rag.nlp import search, rag_tokenizer +from rag.nlp import search, rag_tokenizer, add_positions from rag.raptor import RecursiveAbstractiveProcessing4TreeOrganizedRetrieval as Raptor from rag.settings import DOC_MAXIMUM_SIZE, DOC_BULK_SIZE, EMBEDDING_BATCH_SIZE, SVR_CONSUMER_GROUP_NAME, get_svr_queue_name, get_svr_queue_names, print_rag_settings, TAG_FLD, PAGERANK_FLD from rag.utils import num_tokens_from_string, truncate @@ -88,6 +89,13 @@ FACTORY = { ParserType.TAG.value: tag } +TASK_TYPE_TO_PIPELINE_TASK_TYPE = { + "dataflow" : PipelineTaskType.PARSE, + "raptor": PipelineTaskType.RAPTOR, + "graphrag": PipelineTaskType.GRAPH_RAG, + "mindmap": PipelineTaskType.MINDMAP, +} + UNACKED_ITERATOR = None CONSUMER_NO = "0" if len(sys.argv) < 2 else sys.argv[1] @@ -143,6 +151,7 @@ def start_tracemalloc_and_snapshot(signum, frame): max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss logging.info(f"taken snapshot {snapshot_file}. max RSS={max_rss / 1000:.2f} MB, current memory usage: {current / 10**6:.2f} MB, Peak memory usage: {peak / 10**6:.2f} MB") + # SIGUSR2 handler: stop tracemalloc def stop_tracemalloc(signum, frame): if tracemalloc.is_tracing(): @@ -151,6 +160,7 @@ def stop_tracemalloc(signum, frame): else: logging.info("tracemalloc not running") + class TaskCanceledException(Exception): def __init__(self, msg): self.msg = msg @@ -216,7 +226,14 @@ async def collect(): return None, None canceled = False - task = TaskService.get_task(msg["id"]) + if msg.get("doc_id", "") in [GRAPH_RAPTOR_FAKE_DOC_ID, CANVAS_DEBUG_DOC_ID]: + task = msg + if task["task_type"] in ["graphrag", "raptor", "mindmap"] and msg.get("doc_ids", []): + task = TaskService.get_task(msg["id"], msg["doc_ids"]) + task["doc_ids"] = msg["doc_ids"] + else: + task = TaskService.get_task(msg["id"]) + if task: canceled = has_canceled(task["id"]) if not task or canceled: @@ -228,10 +245,9 @@ async def collect(): task_type = msg.get("task_type", "") task["task_type"] = task_type - if task_type == "dataflow": - task["tenant_id"]=msg.get("tenant_id", "") - task["dsl"] = msg.get("dsl", "") - task["dataflow_id"] = msg.get("dataflow_id", get_uuid()) + if task_type[:8] == "dataflow": + task["tenant_id"] = msg["tenant_id"] + task["dataflow_id"] = msg["dataflow_id"] task["kb_id"] = msg.get("kb_id", "") return redis_msg, task @@ -301,30 +317,8 @@ async def build_chunks(task, progress_callback): d["img_id"] = "" docs.append(d) return - - with BytesIO() as output_buffer: - if isinstance(d["image"], bytes): - output_buffer.write(d["image"]) - output_buffer.seek(0) - else: - # If the image is in RGBA mode, convert it to RGB mode before saving it in JPEG format. - if d["image"].mode in ("RGBA", "P"): - converted_image = d["image"].convert("RGB") - #d["image"].close() # Close original image - d["image"] = converted_image - try: - d["image"].save(output_buffer, format='JPEG') - except OSError as e: - logging.warning( - "Saving image of chunk {}/{}/{} got exception, ignore: {}".format(task["location"], task["name"], d["id"], str(e))) - - async with minio_limiter: - await trio.to_thread.run_sync(lambda: STORAGE_IMPL.put(task["kb_id"], d["id"], output_buffer.getvalue())) - d["img_id"] = "{}-{}".format(task["kb_id"], d["id"]) - if not isinstance(d["image"], bytes): - d["image"].close() - del d["image"] # Remove image reference - docs.append(d) + await image2id(d, partial(STORAGE_IMPL.put), d["id"], task["kb_id"]) + docs.append(d) except Exception: logging.exception( "Saving image of chunk {}/{}/{} got exception".format(task["location"], task["name"], d["id"])) @@ -482,35 +476,192 @@ async def embedding(docs, mdl, parser_config=None, callback=None): return tk_count, vector_size -async def run_dataflow(dsl:str, tenant_id:str, doc_id:str, task_id:str, flow_id:str, callback=None): - _ = callback +async def run_dataflow(task: dict): + task_start_ts = timer() + dataflow_id = task["dataflow_id"] + doc_id = task["doc_id"] + task_id = task["id"] + task_dataset_id = task["kb_id"] - pipeline = Pipeline(dsl=dsl, tenant_id=tenant_id, doc_id=doc_id, task_id=task_id, flow_id=flow_id) - pipeline.reset() + if task["task_type"] == "dataflow": + e, cvs = UserCanvasService.get_by_id(dataflow_id) + assert e, "User pipeline not found." + dsl = cvs.dsl + else: + e, pipeline_log = PipelineOperationLogService.get_by_id(dataflow_id) + assert e, "Pipeline log not found." + dsl = pipeline_log.dsl + dataflow_id = pipeline_log.pipeline_id + pipeline = Pipeline(dsl, tenant_id=task["tenant_id"], doc_id=doc_id, task_id=task_id, flow_id=dataflow_id) + chunks = await pipeline.run(file=task["file"]) if task.get("file") else await pipeline.run() + if doc_id == CANVAS_DEBUG_DOC_ID: + return - await pipeline.run() + if not chunks: + PipelineOperationLogService.create(document_id=doc_id, pipeline_id=dataflow_id, task_type=PipelineTaskType.PARSE, dsl=str(pipeline)) + return + + embedding_token_consumption = chunks.get("embedding_token_consumption", 0) + if chunks.get("chunks"): + chunks = copy.deepcopy(chunks["chunks"]) + elif chunks.get("json"): + chunks = copy.deepcopy(chunks["json"]) + elif chunks.get("markdown"): + chunks = [{"text": [chunks["markdown"]]}] + elif chunks.get("text"): + chunks = [{"text": [chunks["text"]]}] + elif chunks.get("html"): + chunks = [{"text": [chunks["html"]]}] + + keys = [k for o in chunks for k in list(o.keys())] + if not any([re.match(r"q_[0-9]+_vec", k) for k in keys]): + try: + set_progress(task_id, prog=0.82, msg="\n-------------------------------------\nStart to embedding...") + e, kb = KnowledgebaseService.get_by_id(task["kb_id"]) + embedding_id = kb.embd_id + embedding_model = LLMBundle(task["tenant_id"], LLMType.EMBEDDING, llm_name=embedding_id) + @timeout(60) + def batch_encode(txts): + nonlocal embedding_model + return embedding_model.encode([truncate(c, embedding_model.max_length - 10) for c in txts]) + vects = np.array([]) + texts = [o.get("questions", o.get("summary", o["text"])) for o in chunks] + delta = 0.20/(len(texts)//EMBEDDING_BATCH_SIZE+1) + prog = 0.8 + for i in range(0, len(texts), EMBEDDING_BATCH_SIZE): + async with embed_limiter: + vts, c = await trio.to_thread.run_sync(lambda: batch_encode(texts[i : i + EMBEDDING_BATCH_SIZE])) + if len(vects) == 0: + vects = vts + else: + vects = np.concatenate((vects, vts), axis=0) + embedding_token_consumption += c + prog += delta + if i % (len(texts)//EMBEDDING_BATCH_SIZE/100+1) == 1: + set_progress(task_id, prog=prog, msg=f"{i+1} / {len(texts)//EMBEDDING_BATCH_SIZE}") + + assert len(vects) == len(chunks) + for i, ck in enumerate(chunks): + v = vects[i].tolist() + ck["q_%d_vec" % len(v)] = v + except Exception as e: + set_progress(task_id, prog=-1, msg=f"[ERROR]: {e}") + PipelineOperationLogService.create(document_id=doc_id, pipeline_id=dataflow_id, task_type=PipelineTaskType.PARSE, dsl=str(pipeline)) + return + + + metadata = {} + def dict_update(meta): + nonlocal metadata + if not meta: + return + if isinstance(meta, str): + try: + meta = json_repair.loads(meta) + except Exception: + logging.error("Meta data format error.") + return + if not isinstance(meta, dict): + return + for k, v in meta.items(): + if isinstance(v, list): + v = [vv for vv in v if isinstance(vv, str)] + if not v: + continue + if not isinstance(v, list) and not isinstance(v, str): + continue + if k not in metadata: + metadata[k] = v + continue + if isinstance(metadata[k], list): + if isinstance(v, list): + metadata[k].extend(v) + else: + metadata[k].append(v) + else: + metadata[k] = v + + for ck in chunks: + ck["doc_id"] = doc_id + ck["kb_id"] = [str(task["kb_id"])] + ck["docnm_kwd"] = task["name"] + ck["create_time"] = str(datetime.now()).replace("T", " ")[:19] + ck["create_timestamp_flt"] = datetime.now().timestamp() + ck["id"] = xxhash.xxh64((ck["text"] + str(ck["doc_id"])).encode("utf-8")).hexdigest() + if "questions" in ck: + if "question_tks" not in ck: + ck["question_kwd"] = ck["questions"].split("\n") + ck["question_tks"] = rag_tokenizer.tokenize(str(ck["questions"])) + del ck["questions"] + if "keywords" in ck: + if "important_tks" not in ck: + ck["important_kwd"] = ck["keywords"].split(",") + ck["important_tks"] = rag_tokenizer.tokenize(str(ck["keywords"])) + del ck["keywords"] + if "summary" in ck: + if "content_ltks" not in ck: + ck["content_ltks"] = rag_tokenizer.tokenize(str(ck["summary"])) + ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) + del ck["summary"] + if "metadata" in ck: + dict_update(ck["metadata"]) + del ck["metadata"] + if "content_with_weight" not in ck: + ck["content_with_weight"] = ck["text"] + del ck["text"] + if "positions" in ck: + add_positions(ck, ck["positions"]) + del ck["positions"] + + if metadata: + e, doc = DocumentService.get_by_id(doc_id) + if e: + if isinstance(doc.meta_fields, str): + doc.meta_fields = json.loads(doc.meta_fields) + dict_update(doc.meta_fields) + DocumentService.update_by_id(doc_id, {"meta_fields": metadata}) + + start_ts = timer() + set_progress(task_id, prog=0.82, msg="[DOC Engine]:\nStart to index...") + e = await insert_es(task_id, task["tenant_id"], task["kb_id"], chunks, partial(set_progress, task_id, 0, 100000000)) + if not e: + PipelineOperationLogService.create(document_id=doc_id, pipeline_id=dataflow_id, task_type=PipelineTaskType.PARSE, dsl=str(pipeline)) + return + + time_cost = timer() - start_ts + task_time_cost = timer() - task_start_ts + set_progress(task_id, prog=1., msg="Indexing done ({:.2f}s). Task done ({:.2f}s)".format(time_cost, task_time_cost)) + DocumentService.increment_chunk_num(doc_id, task_dataset_id, embedding_token_consumption, len(chunks), task_time_cost) + logging.info("[Done], chunks({}), token({}), elapsed:{:.2f}".format(len(chunks), embedding_token_consumption, task_time_cost)) + PipelineOperationLogService.create(document_id=doc_id, pipeline_id=dataflow_id, task_type=PipelineTaskType.PARSE, dsl=str(pipeline)) @timeout(3600) -async def run_raptor(row, chat_mdl, embd_mdl, vector_size, callback=None): +async def run_raptor_for_kb(row, kb_parser_config, chat_mdl, embd_mdl, vector_size, callback=None, doc_ids=[]): + fake_doc_id = GRAPH_RAPTOR_FAKE_DOC_ID + + raptor_config = kb_parser_config.get("raptor", {}) + chunks = [] vctr_nm = "q_%d_vec"%vector_size - for d in settings.retrievaler.chunk_list(row["doc_id"], row["tenant_id"], [str(row["kb_id"])], - fields=["content_with_weight", vctr_nm]): - chunks.append((d["content_with_weight"], np.array(d[vctr_nm]))) + for doc_id in doc_ids: + for d in settings.retrievaler.chunk_list(doc_id, row["tenant_id"], [str(row["kb_id"])], + fields=["content_with_weight", vctr_nm], + sort_by_position=True): + chunks.append((d["content_with_weight"], np.array(d[vctr_nm]))) raptor = Raptor( - row["parser_config"]["raptor"].get("max_cluster", 64), + raptor_config.get("max_cluster", 64), chat_mdl, embd_mdl, - row["parser_config"]["raptor"]["prompt"], - row["parser_config"]["raptor"]["max_token"], - row["parser_config"]["raptor"]["threshold"] + raptor_config["prompt"], + raptor_config["max_token"], + raptor_config["threshold"], ) original_length = len(chunks) - chunks = await raptor(chunks, row["parser_config"]["raptor"]["random_seed"], callback) + chunks = await raptor(chunks, row["kb_parser_config"]["raptor"]["random_seed"], callback) doc = { - "doc_id": row["doc_id"], + "doc_id": fake_doc_id, "kb_id": [str(row["kb_id"])], "docnm_kwd": row["name"], "title_tks": rag_tokenizer.tokenize(row["name"]) @@ -521,7 +672,7 @@ async def run_raptor(row, chat_mdl, embd_mdl, vector_size, callback=None): tk_count = 0 for content, vctr in chunks[original_length:]: d = copy.deepcopy(doc) - d["id"] = xxhash.xxh64((content + str(d["doc_id"])).encode("utf-8")).hexdigest() + d["id"] = xxhash.xxh64((content + str(fake_doc_id)).encode("utf-8")).hexdigest() d["create_time"] = str(datetime.now()).replace("T", " ")[:19] d["create_timestamp_flt"] = datetime.now().timestamp() d[vctr_nm] = vctr.tolist() @@ -533,8 +684,51 @@ async def run_raptor(row, chat_mdl, embd_mdl, vector_size, callback=None): return res, tk_count +async def delete_image(kb_id, chunk_id): + try: + async with minio_limiter: + STORAGE_IMPL.delete(kb_id, chunk_id) + except Exception: + logging.exception(f"Deleting image of chunk {chunk_id} got exception") + raise + + +async def insert_es(task_id, task_tenant_id, task_dataset_id, chunks, progress_callback): + for b in range(0, len(chunks), DOC_BULK_SIZE): + doc_store_result = await trio.to_thread.run_sync(lambda: settings.docStoreConn.insert(chunks[b:b + DOC_BULK_SIZE], search.index_name(task_tenant_id), task_dataset_id)) + task_canceled = has_canceled(task_id) + if task_canceled: + progress_callback(-1, msg="Task has been canceled.") + return + if b % 128 == 0: + progress_callback(prog=0.8 + 0.1 * (b + 1) / len(chunks), msg="") + if doc_store_result: + error_message = f"Insert chunk error: {doc_store_result}, please check log file and Elasticsearch/Infinity status!" + progress_callback(-1, msg=error_message) + raise Exception(error_message) + chunk_ids = [chunk["id"] for chunk in chunks[:b + DOC_BULK_SIZE]] + chunk_ids_str = " ".join(chunk_ids) + try: + TaskService.update_chunk_ids(task_id, chunk_ids_str) + except DoesNotExist: + logging.warning(f"do_handle_task update_chunk_ids failed since task {task_id} is unknown.") + doc_store_result = await trio.to_thread.run_sync(lambda: settings.docStoreConn.delete({"id": chunk_ids}, search.index_name(task_tenant_id), task_dataset_id)) + async with trio.open_nursery() as nursery: + for chunk_id in chunk_ids: + nursery.start_soon(delete_image, task_dataset_id, chunk_id) + progress_callback(-1, msg=f"Chunk updates failed since task {task_id} is unknown.") + return + return True + + @timeout(60*60*2, 1) async def do_handle_task(task): + task_type = task.get("task_type", "") + + if task_type == "dataflow" and task.get("doc_id", "") == CANVAS_DEBUG_DOC_ID: + await run_dataflow(task) + return + task_id = task["id"] task_from_page = task["from_page"] task_to_page = task["to_page"] @@ -576,32 +770,70 @@ async def do_handle_task(task): init_kb(task, vector_size) - task_type = task.get("task_type", "") - if task_type == "dataflow": - task_dataflow_dsl = task["dsl"] - task_dataflow_id = task["dataflow_id"] - await run_dataflow(dsl=task_dataflow_dsl, tenant_id=task_tenant_id, doc_id=task_doc_id, task_id=task_id, flow_id=task_dataflow_id, callback=None) + if task_type[:len("dataflow")] == "dataflow": + await run_dataflow(task) return - elif task_type == "raptor": + + if task_type == "raptor": + ok, kb = KnowledgebaseService.get_by_id(task_dataset_id) + if not ok: + progress_callback(prog=-1.0, msg="Cannot found valid knowledgebase for RAPTOR task") + return + + kb_parser_config = kb.parser_config + if not kb_parser_config.get("raptor", {}).get("use_raptor", False): + progress_callback(prog=-1.0, msg="Internal error: Invalid RAPTOR configuration") + return # bind LLM for raptor chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language) # run RAPTOR async with kg_limiter: - chunks, token_count = await run_raptor(task, chat_model, embedding_model, vector_size, progress_callback) + chunks, token_count = await run_raptor_for_kb( + row=task, + kb_parser_config=kb_parser_config, + chat_mdl=chat_model, + embd_mdl=embedding_model, + vector_size=vector_size, + callback=progress_callback, + doc_ids=task.get("doc_ids", []), + ) # Either using graphrag or Standard chunking methods elif task_type == "graphrag": - if not task_parser_config.get("graphrag", {}).get("use_graphrag", False): - progress_callback(prog=-1.0, msg="Internal configuration error.") + ok, kb = KnowledgebaseService.get_by_id(task_dataset_id) + if not ok: + progress_callback(prog=-1.0, msg="Cannot found valid knowledgebase for GraphRAG task") return - graphrag_conf = task["kb_parser_config"].get("graphrag", {}) + + kb_parser_config = kb.parser_config + if not kb_parser_config.get("graphrag", {}).get("use_graphrag", False): + progress_callback(prog=-1.0, msg="Internal error: Invalid GraphRAG configuration") + return + + graphrag_conf = kb_parser_config.get("graphrag", {}) start_ts = timer() chat_model = LLMBundle(task_tenant_id, LLMType.CHAT, llm_name=task_llm_id, lang=task_language) with_resolution = graphrag_conf.get("resolution", False) with_community = graphrag_conf.get("community", False) async with kg_limiter: - await run_graphrag(task, task_language, with_resolution, with_community, chat_model, embedding_model, progress_callback) + # await run_graphrag(task, task_language, with_resolution, with_community, chat_model, embedding_model, progress_callback) + result = await run_graphrag_for_kb( + row=task, + doc_ids=task.get("doc_ids", []), + language=task_language, + kb_parser_config=kb_parser_config, + chat_model=chat_model, + embedding_model=embedding_model, + callback=progress_callback, + with_resolution=with_resolution, + with_community=with_community, + ) + logging.info(f"GraphRAG task result for task {task}:\n{result}") progress_callback(prog=1.0, msg="Knowledge Graph done ({:.2f}s)".format(timer() - start_ts)) return + elif task_type == "mindmap": + progress_callback(1, "place holder") + pass + return else: # Standard chunking methods start_ts = timer() @@ -628,41 +860,9 @@ async def do_handle_task(task): chunk_count = len(set([chunk["id"] for chunk in chunks])) start_ts = timer() - doc_store_result = "" - - async def delete_image(kb_id, chunk_id): - try: - async with minio_limiter: - STORAGE_IMPL.delete(kb_id, chunk_id) - except Exception: - logging.exception( - "Deleting image of chunk {}/{}/{} got exception".format(task["location"], task["name"], chunk_id)) - raise - - for b in range(0, len(chunks), DOC_BULK_SIZE): - doc_store_result = await trio.to_thread.run_sync(lambda: settings.docStoreConn.insert(chunks[b:b + DOC_BULK_SIZE], search.index_name(task_tenant_id), task_dataset_id)) - task_canceled = has_canceled(task_id) - if task_canceled: - progress_callback(-1, msg="Task has been canceled.") - return - if b % 128 == 0: - progress_callback(prog=0.8 + 0.1 * (b + 1) / len(chunks), msg="") - if doc_store_result: - error_message = f"Insert chunk error: {doc_store_result}, please check log file and Elasticsearch/Infinity status!" - progress_callback(-1, msg=error_message) - raise Exception(error_message) - chunk_ids = [chunk["id"] for chunk in chunks[:b + DOC_BULK_SIZE]] - chunk_ids_str = " ".join(chunk_ids) - try: - TaskService.update_chunk_ids(task["id"], chunk_ids_str) - except DoesNotExist: - logging.warning(f"do_handle_task update_chunk_ids failed since task {task['id']} is unknown.") - doc_store_result = await trio.to_thread.run_sync(lambda: settings.docStoreConn.delete({"id": chunk_ids}, search.index_name(task_tenant_id), task_dataset_id)) - async with trio.open_nursery() as nursery: - for chunk_id in chunk_ids: - nursery.start_soon(delete_image, task_dataset_id, chunk_id) - progress_callback(-1, msg=f"Chunk updates failed since task {task['id']} is unknown.") - return + e = await insert_es(task_id, task_tenant_id, task_dataset_id, chunks, progress_callback) + if not e: + return logging.info("Indexing doc({}), page({}-{}), chunks({}), elapsed: {:.2f}".format(task_document_name, task_from_page, task_to_page, len(chunks), @@ -685,6 +885,10 @@ async def handle_task(): if not task: await trio.sleep(5) return + + task_type = task["task_type"] + pipeline_task_type = TASK_TYPE_TO_PIPELINE_TASK_TYPE.get(task_type, PipelineTaskType.PARSE) or PipelineTaskType.PARSE + try: logging.info(f"handle_task begin for task {json.dumps(task)}") CURRENT_TASKS[task["id"]] = copy.deepcopy(task) @@ -704,6 +908,13 @@ async def handle_task(): except Exception: pass logging.exception(f"handle_task got exception for task {json.dumps(task)}") + finally: + task_document_ids = [] + if task_type in ["graphrag", "raptor", "mindmap"]: + task_document_ids = task["doc_ids"] + if not task.get("dataflow_id", ""): + PipelineOperationLogService.record_pipeline_operation(document_id=task["doc_id"], pipeline_id="", task_type=pipeline_task_type, fake_document_ids=task_document_ids) + redis_msg.ack() diff --git a/web/public/iconfont.js b/web/public/iconfont.js index d717db4d5..f514c4cfd 100644 --- a/web/public/iconfont.js +++ b/web/public/iconfont.js @@ -1,5 +1,65 @@ (window._iconfont_svg_string_4909832 = - '<svg><symbol id="icon-mcp" viewBox="0 0 1024 1024"><path d="M171.84 477.184l296.192-296.128q39.808-39.808 96.064-39.808 56.32 0 96.128 39.808t39.808 96.064q0 7.488-0.704 14.72 6.848-0.64 13.888-0.64 57.152 0 97.536 40.32 40.32 40.448 40.32 97.536t-40.32 97.472l-269.696 269.696q-4.928 4.928 0 9.856l57.152 57.152a30.464 30.464 0 0 1-43.072 43.072l-57.152-57.152q-19.84-19.84-19.84-48t19.84-48l269.696-269.696q22.528-22.528 22.528-54.4t-22.528-54.4q-22.592-22.592-54.4-22.592-31.936 0-54.464 22.592l-17.28 17.216L435.2 598.336a30.464 30.464 0 0 1-43.008-0.064l-0.192-0.128a30.272 30.272 0 0 1 0.128-42.88L599.68 347.52l17.408-17.408q21.952-22.016 21.952-53.056t-21.952-52.992q-22.016-22.016-53.056-22.016t-52.992 22.016L214.912 520.256a30.464 30.464 0 0 1-43.072-43.072z m567.104-29.44L513.92 672.96q-39.808 39.808-96.128 39.808t-96.128-39.808q-39.808-39.808-39.808-96.128t39.808-96.128l225.088-225.024a30.464 30.464 0 0 1 43.008 43.008L364.8 523.712q-21.952 22.016-21.952 53.056t21.952 53.056q22.016 21.952 53.056 21.952t52.992-21.952l225.088-225.088a30.464 30.464 0 0 1 43.072 43.072z" ></path></symbol><symbol id="icon-list-end" viewBox="0 0 1024 1024"><path d="M128 554.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667zM128 298.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667zM426.666667 725.333333H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h298.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667z" fill="#757590" ></path><path d="M896 213.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667v426.666667c0 25.6-17.066667 42.666667-42.666666 42.666666h-110.933334l12.8-12.8c17.066667-17.066667 17.066667-42.666667 0-59.733333-17.066667-17.066667-42.666667-17.066667-59.733333 0l-85.333333 85.333333c-4.266667 4.266667-8.533333 8.533333-8.533334 12.8-4.266667 8.533333-4.266667 21.333333 0 34.133334 4.266667 4.266667 4.266667 8.533333 8.533334 12.8l85.333333 85.333333c8.533333 8.533333 21.333333 12.8 29.866667 12.8 8.533333 0 21.333333-4.266667 29.866666-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733333l-12.8-12.8H810.666667c72.533333 0 128-55.466667 128-128V256c0-25.6-17.066667-42.666667-42.666667-42.666667z" fill="#757590" ></path></symbol><symbol id="icon-list-start" viewBox="0 0 1024 1024"><path d="M682.666667 469.333333H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667zM682.666667 725.333333H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667zM128 298.666667h298.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667z" fill="#757590" ></path><path d="M810.666667 213.333333h-110.933334l12.8-12.8c17.066667-17.066667 17.066667-42.666667 0-59.733333-17.066667-17.066667-42.666667-17.066667-59.733333 0l-85.333333 85.333333c-4.266667 4.266667-8.533333 8.533333-8.533334 12.8-4.266667 8.533333-4.266667 21.333333 0 34.133334 4.266667 4.266667 4.266667 8.533333 8.533334 12.8l85.333333 85.333333c8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866666-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733333l-12.8-12.8H810.666667c25.6 0 42.666667 17.066667 42.666666 42.666666v426.666667c0 25.6 17.066667 42.666667 42.666667 42.666667s42.666667-17.066667 42.666667-42.666667V341.333333c0-72.533333-55.466667-128-128-128z" fill="#757590" ></path></symbol><symbol id="icon-webcrawler-0" viewBox="0 0 1024 1024"><path d="M972.8 578.1504c0 10.2912-3.6352 19.0976-10.8032 26.4704-6.7328 7.1424-16.128 11.136-25.9328 11.008H807.168c0 66.8672-12.9792 124.2112-38.8352 170.496l119.4752 122.752c7.2448 7.296 10.7776 16.128 10.7776 26.4704 0 10.24-3.5328 19.072-10.7776 26.368a35.0208 35.0208 0 0 1-25.856 11.0848 34.9696 34.9696 0 0 1-25.9584-11.0592l-113.7408-116.0704c-2.176 2.176-5.0688 4.4288-8.704 7.3216-7.936 5.9392-16.0768 11.5712-24.4224 16.896-11.904 8.1152-24.448 15.232-37.504 21.3504a237.6448 237.6448 0 0 1-47.4624 16.8448 208.4352 208.4352 0 0 1-43.008 7.04 12.5696 12.5696 0 0 1-13.1584-12.8256V403.2a13.1584 13.1584 0 0 0-13.1584-13.1584H488.448a13.1584 13.1584 0 0 0-13.1584 13.1584v499.8144a12.5952 12.5952 0 0 1-13.1584 12.7744 237.9008 237.9008 0 0 1-45.1584-7.68 251.648 251.648 0 0 1-50.3552-19.1232 597.2992 597.2992 0 0 1-38.1952-22.784 189.952 189.952 0 0 1-25.1904-19.072l-8.6272-8.0896L189.44 960.256A36.864 36.864 0 0 1 162.048 972.8a35.9424 35.9424 0 0 1-35.9936-36.0192 36.48 36.48 0 0 1 8.704-27.1872l115.84-132.9664c-22.272-44.9024-33.7664-98.56-33.7664-161.024H87.936c-9.8048 0.1536-19.2-3.84-25.9328-10.9824A36.48 36.48 0 0 1 51.2 578.1504c0-10.3168 3.6352-19.072 10.8032-26.4448 6.7072-7.168 16.128-11.1872 25.9328-11.0592H216.832v-173.3888l-99.4048-101.4528a36.48 36.48 0 0 1-10.8032-26.4448c0-10.2656 3.6352-19.0976 10.8032-26.4704 6.7328-7.1168 16.128-11.0592 25.9328-10.9312 10.0864 0 18.688 3.6864 25.9328 11.008l99.4048 101.376h485.9392l99.4048-101.376a35.0464 35.0464 0 0 1 25.856-11.008c10.0864 0 18.688 3.6864 25.9328 11.008 7.2448 7.3728 10.8032 16.1792 10.8032 26.4448 0 10.2656-3.6096 19.0976-10.8032 26.4704l-98.6624 101.2992v172.7488h128.896c10.0608 0 18.7392 3.6864 25.9328 11.0592 7.2448 8.0128 10.7776 16.8448 10.7776 27.1616zM695.8848 226.176a12.5952 12.5952 0 0 1-12.7744 13.184h-342.272a12.6208 12.6208 0 0 1-12.8-13.184c2.8416-46.464 20.6848-86.2208 53.6064-119.808a175.4368 175.4368 0 0 1 130.304-55.1424c51.1744 0 94.3616 18.432 130.3552 55.0912 32.9216 32.9216 50.7648 73.2672 53.5808 119.8592z" fill="currentColor" ></path></symbol><symbol id="icon-retrival-0" viewBox="0 0 1024 1024"><path d="M664.704 645.376a96.0256 96.0256 0 0 0 0 192 96.0256 96.0256 0 0 0 0-192zM818.9952 64H205.0048a64.0512 64.0512 0 0 0-64 64v768c0 35.3024 28.672 64 64 64h613.9904c5.8112 0 11.392-0.7936 16.7936-2.2016l-82.8928-82.8928a158.7968 158.7968 0 0 1-88.192 26.496 159.9488 159.9488 0 0 1-160-160 159.9488 159.9488 0 0 1 160-160 159.9488 159.9488 0 0 1 133.504 248.192l82.688 82.816c1.408-5.1968 2.0992-10.7008 2.0992-16.4096V128c0-35.3024-28.672-64-64-64z m-348.416 438.784c-5.7856 5.8112-13.7728 9.4208-22.5792 9.4208l-128-0.2048c-17.6128 0-32-14.4128-32-32 0-8.8064 3.584-16.7936 9.3952-22.6048 5.8112-5.7856 13.824-9.3952 22.6048-9.3952l128.1024 0.2048c17.5872 0 32 14.3872 32 32a32.4096 32.4096 0 0 1-9.4976 22.6048z m256-128.1792c-5.7856 5.7856-13.7728 9.3952-22.5792 9.3952h-384c-17.6128 0-32-14.4128-32-32 0-8.8064 3.584-16.7936 9.3952-22.6048 5.8112-5.7856 13.824-9.3952 22.6048-9.3952h384c17.6128 0 32 14.4128 32 32 0 8.8064-3.584 16.7936-9.3952 22.6048z m0-128c-5.7856 5.7856-13.7728 9.3952-22.5792 9.3952h-384c-17.6128 0-32-14.4128-32-32 0-8.8064 3.584-16.7936 9.3952-22.6048 5.8112-5.7856 13.824-9.3952 22.6048-9.3952h384c17.6128 0 32 14.4128 32 32 0 8.8064-3.584 16.7936-9.3952 22.6048z" fill="currentColor" ></path></symbol><symbol id="icon-runcode-0" viewBox="0 0 1024 1024"><path d="M446.264832 920.6784l-97.5872-28.3136a19.2256 19.2256 0 0 1-13.1072-23.8336l218.368-752.2816a19.2256 19.2256 0 0 1 23.8592-13.1072l97.5872 28.3136c10.24 2.8928 16 13.5936 13.1072 23.8336l-218.368 752.256a19.1744 19.1744 0 0 1-23.8592 13.1328z m-182.3744-179.5072l69.6064-74.24a19.2 19.2 0 0 0-1.28-27.52l-144.9728-127.488 144.9728-127.5136a19.072 19.072 0 0 0 1.28-27.52l-69.6064-74.24a19.2 19.2 0 0 0-27.1872-0.7936L6.149632 497.8176a19.072 19.072 0 0 0 0 28.0064l230.5536 216.1408a19.072 19.072 0 0 0 27.1872-0.7936z m523.4688 0.9472l230.5536-216.1408a19.072 19.072 0 0 0 0-27.9808l-230.5536-216.32a19.3792 19.3792 0 0 0-27.1872 0.8192l-69.6064 74.24a19.2 19.2 0 0 0 1.28 27.4944l144.9728 127.6672-144.9728 127.5136a19.072 19.072 0 0 0-1.28 27.52l69.6064 74.24a19.2 19.2 0 0 0 27.1872 0.9472z" fill="currentColor" ></path></symbol><symbol id="icon-sendemail-0" viewBox="0 0 1024 1024"><path d="M798.72 524.4928a19.328 19.328 0 0 1 20.48 4.1472l198.656 178.9952c8.192 8.32 8.192 20.8128 0 27.0848L819.2 915.7632a18.4832 18.4832 0 0 1-14.336 6.272c-2.048 0-4.096 0-6.144-2.0992-6.144-2.0992-10.24-8.32-10.24-16.64v-99.9168c-163.84 0-239.616 14.592-294.912 220.6208 0-320.512 167.936-382.976 294.912-382.976v-97.792c0-8.32 4.096-14.592 10.24-18.7648v0.0256zM829.44 0c65.536 0 118.784 54.0928 118.784 120.7552v401.664c0 12.4928-2.048 24.96-6.144 35.4048l-81.92-72.8832c-18.432-18.7648-53.248-27.0336-81.92-14.592l-145.408-151.8848 274.432-218.5728-434.176 237.2608L38.912 99.9168l276.48 218.496-227.328 237.2864 276.48-201.856 108.544 72.832 108.544-72.832 176.128 126.9248c-18.432 14.592-28.672 37.4528-28.672 62.464v41.6c-45.056 6.272-98.304 22.912-145.408 58.2912H118.784c-65.536 0-118.784-54.1184-118.784-120.7552V120.7552C0 54.1952 53.248 0 118.784 0H829.44z" fill="currentColor" ></path></symbol><symbol id="icon-httprequest-0" viewBox="0 0 1024 1024"><path d="M805.248 51.2C916.9408 51.2 972.8 107.0592 972.8 218.752v586.496C972.8 916.9408 916.9408 972.8 805.248 972.8H218.752C107.0592 972.8 51.2 916.9408 51.2 805.248V218.752C51.2 107.0592 107.0592 51.2 218.752 51.2h586.496z m-18.0224 609.1776a20.9408 20.9408 0 0 0-19.712 7.4496 325.1968 325.1968 0 0 1-166.016 106.8544c16.0768-22.656 29.7984-51.4048 40.8576-84.1728a20.9408 20.9408 0 0 0-39.68-13.4656c-12.2112 36.1984-27.0336 64.3072-42.8032 82.944-15.7952 18.7648-30.5664 25.9072-43.3408 25.9072-12.8 0-27.5712-7.168-43.3664-25.9072-13.7728-16.2816-26.8544-39.8848-38.0672-69.7344l-4.7104-13.1584-1.7408-3.8912a20.9408 20.9408 0 0 0-38.8864 13.184l0.9216 4.096 5.12 14.3872c10.112 26.8544 22.0928 50.56 35.7632 69.7856a325.0432 325.0432 0 0 1-166.0416-106.8288l-1.9712-2.048a20.9408 20.9408 0 0 0-30.2848 28.7744l6.4256 7.5776a367.104 367.104 0 0 0 276.8384 125.6448 367.104 367.104 0 0 0 283.264-133.2224 20.9408 20.9408 0 0 0-12.544-34.176zM210.048 411.8784H159.744v201.3696h50.304V529.92h67.6864v83.3536h50.3296V411.904h-50.3296v67.6608H210.048v-67.6608z m638.7456-0.0512l-117.9648 0.0512v201.3184h50.3552v-67.6352h67.6608a48.1792 48.1792 0 0 0 35.4048-14.9504c9.9328-9.984 14.9504-21.7856 14.9504-35.4048v-33.024c0-13.6448-4.992-25.472-14.9504-35.4304a48.5632 48.5632 0 0 0-35.456-14.9248z m-151.0144 0h-151.0144v50.3552h50.3552v150.9888h50.3552v-151.04h50.304v-50.304z m-185.6512 0h-151.04v50.3552h50.3808v150.9888h50.304v-151.04h50.3552v-50.304z m336.6656 50.3296v33.0752h-67.584v-33.0752h67.584zM516.5056 196.224A367.104 367.104 0 0 0 233.2928 329.472l-1.664 2.2528a20.9408 20.9408 0 0 0 33.92 24.4736l6.0672-7.168a325.1968 325.1968 0 0 1 160.0512-99.712c-13.7216 19.2512-25.728 43.008-35.84 69.888l-5.12 14.3616a20.9664 20.9664 0 0 0 39.7056 13.4144l4.6592-13.1584c11.3152-29.8752 24.32-53.4016 38.144-69.76 15.7696-18.7648 30.5664-25.856 43.3408-25.856 12.7744 0 27.5456 7.1424 43.3408 25.856 13.7984 16.3072 26.88 39.8848 38.144 69.76l4.6848 13.1584 1.7408 3.8912a20.9408 20.9408 0 0 0 38.8864-13.2096l-0.9728-4.096-5.12-14.3616c-10.112-26.88-22.0928-50.6368-35.84-69.888a325.0432 325.0432 0 0 1 154.5728 93.6448l11.5712 13.2352a20.9408 20.9408 0 0 0 34.5856-23.2192l-2.3296-3.5072a367.104 367.104 0 0 0-283.264-133.2224z" fill="currentColor" ></path></symbol><symbol id="icon-executesql-0" viewBox="0 0 1024 1024"><path d="M64 649.2928c96 77.6192 269.6448 118.272 443.136 118.272 173.568 0 347.2128-40.6528 443.2128-118.2208v136.6528h-1.92c-22.1696 94.208-210.432 166.1952-441.2928 166.1952-230.784 0-419.072-72.0128-441.2672-166.1952H64v-136.704z m129.28 118.272a37.0688 37.0688 0 0 0-36.992 36.864 36.992 36.992 0 0 0 73.9072 0 37.0688 37.0688 0 0 0-36.992-36.864h0.0768z m757.0688-376.7808v118.1952c0 101.5808-197.632 184.6528-443.2128 184.6528-245.504 0-443.136-82.9952-443.136-184.6272v-118.2208c96 77.696 269.6448 118.1952 443.136 118.1952 173.568 0 347.2128-40.6272 443.2128-118.1952zM193.2288 508.9792a37.0432 37.0432 0 0 0-36.9408 36.9408 36.992 36.992 0 0 0 73.9072 0 37.0432 37.0432 0 0 0-36.992-36.9152zM551.4752 64c43.9552 1.92 87.808 6.784 131.072 14.72 16.64 3.7632 33.28 7.424 48 11.136 22.5792 5.4528 44.8 12.16 66.56 20.352 20.2752 9.1392 40.4992 16.5632 57.216 25.8048 7.3472 5.5552 16.5632 9.216 23.9104 14.72 3.712 1.92 7.5008 5.5552 11.136 7.424l5.4528 3.6864c3.584 2.5088 6.8352 4.9408 9.2928 7.3728 4.1472 2.56 7.8592 5.632 11.0592 9.2672l16.64 20.3008c12.928 16.64 18.5088 33.28 18.5088 51.712 0 101.5552-197.632 184.6528-443.2128 184.6528C261.632 435.2 64 352.128 64 250.496c0-15.6928 4.4032-31.0528 12.8512-44.288 6.2976-10.112 13.7216-19.456 22.2208-27.776 12.8512-12.8 27.6992-23.936 46.208-34.9952l18.432-11.0848c16.64-7.424 33.28-14.72 53.5808-22.144 20.2752-7.424 42.4192-12.928 64.6144-18.56 14.72-3.584 31.3856-5.504 48-9.1392a943.5648 943.5648 0 0 1 131.072-14.7968c16.64-1.92 31.3856-1.92 46.1568-1.92 14.72 0 29.6448 0 44.3648-1.792z" fill="currentColor" ></path></symbol><symbol id="icon-less-or-equal" viewBox="0 0 1024 1024"><path d="M320.731429 441.636571L730.697143 266.605714 730.185143 219.428571l-520.045714 222.134858 520.045714 228.205714 0.438857-52.443429L320.731429 441.782857zM730.697143 804.571429l0.804571-50.176-518.802285-232.009143-2.486858 50.029714L730.550857 804.571429z" fill="#939393" ></path></symbol><symbol id="icon-not-contains" viewBox="0 0 1024 1024"><path d="M579.291429 146.285714c18.432 19.017143 25.380571 34.596571 20.992 46.738286l-33.206858 90.624h22.162286c114.688 0 208.457143 84.845714 214.966857 197.485714L804.571429 493.714286c0 118.857143-96.402286 209.993143-215.332572 209.993143H413.622857l-51.053714 139.922285a35.035429 35.035429 0 1 1-65.755429-24.137143l41.691429-115.785142H219.428571v-59.977143L360.155429 643.657143l108.251428-300.032H219.428571v-59.977143l270.628572-0.073143 42.057143-116.443428c4.388571-12.068571 20.187429-19.017143 47.177143-20.918858zM435.565714 643.730286h153.673143c89.234286 0 161.499429-60.708571 161.499429-149.942857 0-89.234286-72.265143-150.089143-161.499429-150.089143h-44.105143L435.565714 643.730286z" fill="#939393" ></path></symbol><symbol id="icon-Less" viewBox="0 0 1024 1024"><path d="M339.529143 511.122286L804.571429 749.202286l-0.073143 55.369143-569.563429-293.595429L804.571429 219.428571v57.124572z" fill="#939393" ></path></symbol><symbol id="icon-Greater-or-equal" viewBox="0 0 1024 1024"><path d="M630.125714 441.636571L220.16 266.605714 220.672 219.428571l520.045714 222.134858-520.045714 228.205714-0.438857-52.443429L630.125714 441.782857zM220.16 804.571429L219.428571 754.395429l518.802286-232.009143 2.486857 50.029714L220.306286 804.571429z" fill="#939393" ></path></symbol><symbol id="icon-equal" viewBox="0 0 1024 1024"><path d="M219.428571 545.792h585.142858v44.982857H219.428571v-44.982857zM219.428571 365.714286h585.142858v44.982857H219.428571V365.714286z" fill="#939393" ></path></symbol><symbol id="icon-not-equals" viewBox="0 0 1024 1024"><path d="M462.116571 578.56l86.308572-134.875429H219.428571v-44.909714h357.814858L692.077714 219.428571h51.2l-112.64 179.346286h172.836572v44.909714H601.819429L515.510857 578.56h287.963429v44.909714H486.692571L370.614857 804.571429h-51.638857l114.322286-181.174858H219.428571V578.56z" fill="#939393" ></path></symbol><symbol id="icon-Contains" viewBox="0 0 1024 1024"><path d="M219.428571 352.548571h369.810286c89.234286 0 161.499429 60.854857 161.499429 150.089143 0 89.234286-72.338286 150.016-161.499429 150.016H219.428571v59.977143h369.810286C708.169143 712.704 804.571429 621.641143 804.571429 502.710857S708.169143 292.571429 589.238857 292.571429H219.428571v59.977142z" fill="#939393" ></path></symbol><symbol id="icon-KR" viewBox="0 0 1024 1024"><path d="M440.4736 204.8a76.8 76.8 0 0 1 60.7744 29.7984l36.352 47.0016 307.6096 1.4336A76.8 76.8 0 0 1 921.6 359.8336V742.4a76.8 76.8 0 0 1-76.8 76.8h-614.4A76.8 76.8 0 0 1 153.6 742.4v-460.8A76.8 76.8 0 0 1 230.4 204.8h210.0736zM537.6 416a115.2 115.2 0 1 0 58.4704 214.4768l36.352 36.3008a28.7744 28.7744 0 0 0 40.7552-40.7552l-36.352-36.352A115.2 115.2 0 0 0 537.6 416z m0 57.6a57.6 57.6 0 1 1 0 115.2 57.6 57.6 0 0 1 0-115.2zM440.4736 262.4H230.4a19.2 19.2 0 0 0-18.8928 15.7696l-0.3072 3.84h253.8496L455.68 269.824a19.2 19.2 0 0 0-15.1552-7.4752z" fill="currentColor" ></path></symbol><symbol id="icon-KR1" viewBox="0 0 1024 1024"><path d="M440.4736 204.8a76.8 76.8 0 0 1 60.7744 29.7984l36.352 47.0016 307.6096 1.4336A76.8 76.8 0 0 1 921.6 359.8336V742.4a76.8 76.8 0 0 1-76.8 76.8h-614.4A76.8 76.8 0 0 1 153.6 742.4v-460.8A76.8 76.8 0 0 1 230.4 204.8h210.0736zM537.6 416a115.2 115.2 0 1 0 58.4704 214.4768l36.352 36.3008a28.7744 28.7744 0 0 0 40.7552-40.7552l-36.352-36.352A115.2 115.2 0 0 0 537.6 416z m0 57.6a57.6 57.6 0 1 1 0 115.2 57.6 57.6 0 0 1 0-115.2zM440.4736 262.4H230.4a19.2 19.2 0 0 0-18.8928 15.7696l-0.3072 3.84h253.8496L455.68 269.824a19.2 19.2 0 0 0-15.1552-7.4752z" fill="#FFFFFF" ></path></symbol><symbol id="icon-code-set" viewBox="0 0 1024 1024"><path d="M808.0384 80.0768c20.48 18.5856 37.2224 35.328 50.2784 46.5408 13.0048 13.0048 22.3232 24.1664 27.904 33.4848 7.4752 9.3184 11.1616 16.7936 13.056 22.3744 1.8432 5.5808 1.8432 11.1616 1.8432 16.7424v18.6368h-171.264c-9.3184 0-14.8992-3.7376-22.3744-9.3184-7.424-5.632-11.1616-13.056-14.848-20.48-3.7376-7.4752-7.4752-14.8992-9.3696-24.2176-1.8432-9.3184-3.6864-14.848-3.6864-20.48V0h1.8432c7.4752 0 13.056 0 20.48 1.8432 5.632 1.8944 13.056 5.632 22.3232 11.2128 11.2128 5.5808 22.3744 14.848 35.3792 24.1664 13.056 11.1616 27.9552 24.2176 48.4352 42.8544zM608.768 145.2032c0 14.8992 1.8432 31.6928 7.424 48.4352 5.632 16.7424 11.2128 33.4848 22.3744 46.5408 11.1616 13.0048 22.3232 26.0608 39.0656 35.328 14.8992 9.3696 33.536 14.9504 54.016 14.9504h171.264V875.008c0 22.3232-3.6864 42.8032-13.0048 61.44s-20.48 33.4848-35.3792 46.5408c-14.8992 13.056-31.6416 22.3232-48.4352 29.7984-16.7424 7.424-35.328 11.1616-52.1216 11.1616H251.392c-14.848 0-31.6416-3.7376-48.384-13.056a281.088 281.088 0 0 1-48.4352-33.4848c-14.848-14.8992-27.904-29.7984-37.2224-48.4352-9.3184-18.5856-14.8992-37.2224-14.8992-55.808V150.784c0-16.7424 3.7376-33.4848 11.1616-52.1216 7.4752-18.5856 18.6368-33.4848 31.6416-48.384 13.056-14.8992 27.9552-26.112 44.6976-35.3792C206.6432 5.5808 223.4368 0 242.0224 0h374.272c-7.4752 0-7.4752 145.2032-7.4752 145.2032z m-94.976 526.8992c9.3184-9.3184 14.8992-22.3232 14.8992-37.2224 0-14.848-3.6864-27.904-13.0048-35.328l-189.952-193.6896a47.6672 47.6672 0 0 0-35.328-14.848c-13.056 0-24.2176 5.5296-35.3792 14.848a50.8416 50.8416 0 0 0-14.8992 35.3792c0 13.056 5.5808 24.2176 14.848 35.3792L411.4944 634.88l-154.5216 158.208a47.6672 47.6672 0 0 0-14.848 35.4304c0 14.848 5.5296 26.0608 14.848 35.328 9.3184 9.3696 20.48 14.9504 35.3792 14.9504 13.056 0 24.2176-5.632 35.3792-14.8992l186.1632-191.7952z m240.1792 130.3552h-219.648v74.4448h219.648v-74.4448z" fill="currentColor" ></path></symbol><symbol id="icon-a-ContentRewrite" viewBox="0 0 1024 1024"><path d="M842.9568 499.8144l82.176 82.176a24.1664 24.1664 0 0 1 0 34.1504l-301.568 301.568a23.8592 23.8592 0 0 1-11.4176 6.3488l-115.2 27.7504a24.1664 24.1664 0 0 1-28.8256-30.3616L491.52 842.752v0.1024l9.1648-30.7712a24.6784 24.6784 0 0 1 1.4848-3.7376l-1.4848 3.6864a23.6544 23.6544 0 0 1 6.0928-10.1888l302.0288-302.0288a24.1664 24.1664 0 0 1 34.1504 0z m-177.6128-418.2016v0.0512l51.968 0.0512c55.1936 0 99.9424 44.7488 99.9424 99.9424h0.0512V415.744l-361.0112 348.928-31.0272 106.7008h-0.1024l-9.3696 32.3584H216.8832a99.9424 99.9424 0 0 1-99.9424-99.9424V181.6576c0-22.5792 7.4752-43.4176 20.1216-60.16 18.2784-24.2688 47.2576-39.8848 79.872-39.8848z m-222.6688 478.5664H298.752a49.664 49.664 0 0 0-0.0512 99.328h143.9744a49.664 49.664 0 1 0 0-99.328z m192.8192-164.7616H298.7008a49.664 49.664 0 0 0-44.5952 27.8016l-2.1504 5.0688a49.664 49.664 0 0 0 46.7456 66.4064h193.1264l-0.1024 0.1024 143.8208-0.0512a49.664 49.664 0 1 0-0.0512-99.328zM298.7008 242.432a49.5104 49.5104 0 0 0-48.9984 57.7536l-0.3072-2.2016a49.152 49.152 0 0 0 1.024 5.8368l-0.7168-3.6352a49.408 49.408 0 0 0 1.28 5.6832l-0.512-2.048c0.4608 1.8944 1.024 3.7888 1.6896 5.5808l-1.1776-3.5328c0.6144 2.048 1.3312 4.096 2.2016 5.9904l-1.024-2.4576c0.5632 1.536 1.2288 3.072 1.9456 4.5056l-0.9216-2.048c0.7168 1.6896 1.536 3.328 2.4576 4.9152l-1.536-2.8672a49.664 49.664 0 0 0 3.3792 5.8368l-1.8432-2.9696a49.664 49.664 0 0 0 3.2768 4.9152l-1.4336-1.9456c0.8704 1.28 1.792 2.56 2.816 3.7376l-1.3824-1.792c1.1776 1.5872 2.4576 3.072 3.7888 4.5568l-2.4064-2.7648c1.28 1.5872 2.7136 3.072 4.1984 4.5568l-1.792-1.792a49.5104 49.5104 0 0 0 35.9936 15.4112h301.1584v0.0512h35.6864a49.664 49.664 0 1 0-0.0512-99.328z" fill="currentColor" ></path></symbol><symbol id="icon-await" viewBox="0 0 1024 1024"><path d="M814.1824 138.7008a100.4032 100.4032 0 0 1 34.2528 6.144l1.1264 0.4096a98.0992 98.0992 0 0 1 21.8624 11.776l1.8432 1.3824c1.6896 1.2288 3.2768 2.5088 4.864 3.84l1.3824 1.2288a85.0944 85.0944 0 0 1 11.3664 11.8272l-5.2736-5.9904c17.0496 17.7664 27.4432 41.8816 27.4432 68.4544v476.8256a99.072 99.072 0 0 1-99.072 99.072h-168.0896a32.4608 32.4608 0 0 0-22.9888 9.6256l-79.616 80.5376a32.4096 32.4096 0 0 1-47.0016-1.024l-71.2192-78.5408a32.3584 32.3584 0 0 0-23.9616-10.5984H210.8416a99.1232 99.1232 0 0 1-99.1232-99.1232V237.824c0-54.7328 44.3904-99.1232 99.1232-99.1232zM291.9936 427.8784l-6.4 0.3584a58.88 58.88 0 1 0 6.4-0.3584z m224.2048 0l-6.3488 0.3584a58.88 58.88 0 0 0-52.224 65.3312l-0.2048-3.072c0.1536 2.0992 0.4096 4.1984 0.7168 6.2976l-0.512-3.2256c0.3072 2.304 0.7168 4.5568 1.2288 6.7584l-0.7168-3.584c0.4096 2.4064 0.9728 4.6592 1.6384 6.912l-0.9216-3.328c0.512 2.2016 1.1776 4.352 1.9456 6.5024l-1.024-3.1744a58.9312 58.9312 0 1 0 91.2896-64.4096l-1.1264-0.7168a59.0336 59.0336 0 0 0-12.6976-6.8096l0.6144 0.3072a58.368 58.368 0 0 0-6.4-2.1504l5.7856 1.8432a58.4704 58.4704 0 0 0-6.8608-2.1504l1.0752 0.3072a58.7264 58.7264 0 0 0-15.2576-1.9968z m224.3072 0l-6.4 0.3584a58.88 58.88 0 1 0 6.4-0.3584z" fill="currentColor" ></path></symbol><symbol id="icon-a-QuestionClassification" viewBox="0 0 1024 1024"><path d="M607.0272 127.1808a51.0976 51.0976 0 0 0-87.6544 0l-153.6 256A51.2 51.2 0 0 0 409.6 460.6464h307.2a51.0464 51.0464 0 0 0 43.8272-77.4656l-153.6-256zM563.2 601.6v230.4c0 35.328 28.672 64 64 64h230.4c35.328 0 64-28.672 64-64v-230.4c0-35.328-28.672-64-64-64h-230.4c-35.328 0-64 28.672-64 64zM307.2 921.6a204.8 204.8 0 1 0 0-409.6 204.8 204.8 0 0 0 0 409.6z" fill="currentColor" ></path></symbol><symbol id="icon-condition" viewBox="0 0 1024 1024"><path d="M927.4368 267.8784L801.7408 144.384h-259.584V64H427.8784v80.384H219.2384v241.3056H801.792l125.6448-117.76zM427.8784 960h114.2784v-241.3056H427.8784v241.3056z m-331.264-404.9408l125.6448 117.76H804.864V431.616H222.2592l-125.696 123.4432zM763.904 472.9856v158.5152H235.9296L151.04 555.008l84.8384-82.0736h528.0768z" fill="currentColor" ></path></symbol><symbol id="icon-agent-ai" viewBox="0 0 1024 1024"><path d="M121.2416 347.9552l343.296 199.3728v278.1696h-0.0512v119.1424a110.592 110.592 0 0 1-5.4272-2.9184l-287.0784-166.7072a103.7824 103.7824 0 0 1-51.712-90.0608l0.8192-331.9808c0-1.6896 0.0512-3.328 0.1536-5.0176z m782.3872 2.0992c0.1024 1.5872 0.1536 3.2256 0.1536 4.864l-0.256 91.9552-0.6144 240.0256a103.7824 103.7824 0 0 1-52.1216 89.8048l-287.9488 165.2736-3.2768 1.7408h-0.0512v-395.4176l344.064-198.2464v0.0512zM461.1584 97.8944a103.7824 103.7824 0 0 1 103.8336 0.256l189.5424 110.08 97.5872 56.6784 4.1984 2.6624-343.4496 197.888h-0.0512l-343.808-199.68c1.3824-0.9216 2.7648-1.792 4.1984-2.6112z" fill="currentColor" ></path></symbol><symbol id="icon-reply" viewBox="0 0 1024 1024"><path d="M666.7776 169.5744c26.4192 0 50.5344 9.5232 69.2736 25.2928a99.4816 99.4816 0 0 1 10.24 9.8304l-4.5056-4.608 2.7648 2.7648 1.7408 1.8432v0.0512c9.984 10.8544 17.8176 23.8592 22.6816 38.1952h21.76a144.5888 144.5888 0 0 1 54.8864 10.9568l2.2016 0.9216a142.1312 142.1312 0 0 1 46.1312 32.256c8.1408 8.4992 15.2064 17.92 21.0432 28.16l1.6896 3.072a112.2304 112.2304 0 0 1 7.1168 15.36l-3.2256-7.5776c0.7168 1.4848 1.3824 2.9696 1.9968 4.5056l1.2288 3.072v0.1024c6.4 16.1792 9.8816 33.792 9.8816 52.1728v290.6624a135.0656 135.0656 0 0 1-134.912 134.8608 5.632 5.632 0 0 0-5.5808 5.5808v23.808c0 32.1536-18.2784 60.2112-47.7696 73.1136a79.5136 79.5136 0 0 1-86.1696-14.3872l-94.2592-86.6816a5.5296 5.5296 0 0 0-3.7888-1.4848H450.7136a42.496 42.496 0 0 1-6.5536-0.512l-1.536-0.3072a32.3072 32.3072 0 0 1-3.7888-0.8704l-0.5632-0.2048a34.4064 34.4064 0 0 1-4.608-1.6896l-0.8704-0.4608a30.1568 30.1568 0 0 1-3.4304-1.7408l-1.3312-0.8704a29.696 29.696 0 0 1-3.072-2.048l-1.1264-1.024a31.9488 31.9488 0 0 1-2.9184-2.56l-0.8192-0.8704a34.304 34.304 0 0 1-2.7648-3.1744l-0.6144-0.8192a34.304 34.304 0 0 1-2.048-2.9696l-1.024-1.792a41.0624 41.0624 0 0 1-5.2736-20.4288c0-23.3472 18.9952-42.3424 42.3424-42.3424h110.5408c22.6816 0 44.4416 8.4992 61.1328 23.8592 0.3072 0.256 0.256 0.256 0.256 0.3072l85.8112 78.8992v-12.7488c0-49.8176 40.4992-90.3168 90.3168-90.3168 27.648 0 50.176-22.528 50.176-50.176V385.8944a58.368 58.368 0 0 0-58.2656-58.2656h-15.872v253.5424a108.032 108.032 0 0 1-108.032 108.032H443.7504c-12.9024 0-25.344 4.864-34.8672 13.6192l-101.2224 93.0816a39.68 39.68 0 0 1-22.6304 10.496l-4.7616 0.2048a40.2944 40.2944 0 0 1-40.1408-40.3456v-25.5488c0-28.4672-23.0912-51.5072-51.5072-51.5072a99.328 99.328 0 0 1-99.328-99.328V277.76a108.032 108.032 0 0 1 108.032-108.1856z m-153.7024 303.9744h-239.104a39.5776 39.5776 0 1 0 0 79.2576h173.312l-0.1024 0.0512 65.8944-0.0512a39.6288 39.6288 0 0 0 30.1568-65.3312l2.4576 3.2256a39.7824 39.7824 0 0 0-3.3792-4.2496l0.9216 1.024a40.1408 40.1408 0 0 0-3.5328-3.584l2.6112 2.56a39.8848 39.8848 0 0 0-4.1984-3.9936l1.5872 1.3824a40.0384 40.0384 0 0 0-4.0448-3.2256l2.4576 1.8432a39.7312 39.7312 0 0 0-4.1984-2.9696l1.7408 1.1264a39.7824 39.7824 0 0 0-4.7616-2.816l3.072 1.6896a39.5264 39.5264 0 0 0-4.7104-2.5088l1.6384 0.8192a39.424 39.424 0 0 0-5.632-2.304l3.9936 1.4848a39.3728 39.3728 0 0 0-5.0176-1.8432l1.024 0.3584a39.2704 39.2704 0 0 0-4.7104-1.2288l3.6864 0.8704a39.424 39.424 0 0 0-5.6832-1.2288l-5.4784-0.3584zM237.4656 374.0672a39.6288 39.6288 0 0 0 36.5056 24.2176h319.2832a39.7312 39.7312 0 0 0 11.008-1.536l-1.6896 0.4096a39.3216 39.3216 0 0 0 4.7616-1.4336l-3.072 1.024a39.6288 39.6288 0 0 0 5.632-2.048l-2.56 1.024a39.424 39.424 0 0 0 5.12-2.3552l-2.56 1.3312a39.6288 39.6288 0 0 0 4.7104-2.6112l-2.2016 1.28a39.8336 39.8336 0 0 0 15.872-16.0768l-1.8432 3.0208a39.5776 39.5776 0 0 0 2.6112-4.608l-0.768 1.536a39.6288 39.6288 0 0 0 2.1504-4.864l-1.3824 3.2768a39.3728 39.3728 0 0 0 2.2016-5.632l-0.8192 2.3552a39.6288 39.6288 0 0 0 1.5872-5.4784l-0.768 3.1232a39.424 39.424 0 0 0 1.2288-5.7344l0.1536-2.4576 0.256-3.1744a40.192 40.192 0 0 0-0.4096-5.7856l0.3072 2.816a39.6288 39.6288 0 0 0-0.6144-4.608l0.3072 1.792a39.6288 39.6288 0 0 0-1.3824-5.9904l1.024 4.1984a39.3216 39.3216 0 0 0-1.4848-5.632l0.4608 1.4336a39.6288 39.6288 0 0 0-2.304-5.6832l1.8432 4.3008a39.424 39.424 0 0 0-2.2528-5.1712l0.4096 0.8704a39.6288 39.6288 0 0 0-2.9696-5.12l2.56 4.2496a39.6288 39.6288 0 0 0-3.2768-5.2224l-3.84-4.4032a39.6288 39.6288 0 0 0-5.4784-4.6592l0.9728 0.7168a39.6288 39.6288 0 0 0-5.376-3.2768l4.4032 2.56a39.6288 39.6288 0 0 0-9.7792-4.864l5.12 2.0992a39.3728 39.3728 0 0 0-5.888-2.4064l0.768 0.3072a39.6288 39.6288 0 0 0-6.0416-1.536l5.2736 1.2288a39.3728 39.3728 0 0 0-5.888-1.3824l-6.0928-0.4096H273.9712a39.6288 39.6288 0 0 0-7.8848 0.768l2.1504-0.3584a39.6288 39.6288 0 0 0-5.7344 1.28l3.584-0.9216a39.3216 39.3216 0 0 0-5.5296 1.536l1.9456-0.6144a39.6288 39.6288 0 0 0-5.7856 2.2528l3.84-1.5872a39.424 39.424 0 0 0-5.2224 2.304l1.3824-0.7168a39.6288 39.6288 0 0 0-4.8128 2.7648l3.4304-2.048a39.68 39.68 0 0 0-5.1712 3.2768l-4.1984 3.6864a39.6288 39.6288 0 0 0-4.864 5.888l1.1776-1.6896a39.6288 39.6288 0 0 0-3.2768 5.1712l2.048-3.4816a39.5776 39.5776 0 0 0-2.7648 4.864l0.7168-1.3824a39.6288 39.6288 0 0 0-2.304 5.2224l1.5872-3.84a39.3728 39.3728 0 0 0-2.2528 5.7856l0.6656-1.9456a39.6288 39.6288 0 0 0-1.536 5.5296l0.8704-3.584a39.424 39.424 0 0 0-1.28 5.7856l-0.2048 2.3552-0.2048 3.328a39.168 39.168 0 0 0 0.3584 5.3248l1.024 4.864 1.024 3.4816 0.6144 1.4336z" fill="currentColor" ></path></symbol><symbol id="icon-a-textprocessing" viewBox="0 0 1024 1024"><path d="M751.616 102.4a102.4 102.4 0 0 1 102.4 102.4v640a102.4 102.4 0 0 1-102.4 102.4H256a102.4 102.4 0 0 1-102.4-102.4V204.8a102.4 102.4 0 0 1 102.4-102.4h495.616zM370.688 307.2h-43.4176l-10.5472 16.1792a405.0944 405.0944 0 0 0-39.7312 84.992A396.9536 396.9536 0 0 0 256 538.112c0 44.3904 6.8608 87.808 20.992 129.2288 11.8784 34.56 28.3136 68.096 50.2784 100.6592h43.3664l-9.216-17.2032a499.8656 499.8656 0 0 1-34.6112-84.9408 419.8912 419.8912 0 0 1 0-256.512c10.0352-34.048 24.6272-68.096 43.8272-102.144z m326.0416 0h-43.3664l10.752 20.4288c13.6704 27.2384 24.6784 54.4768 33.0752 81.7152 13.2608 42.3936 20.1216 85.3504 20.1216 128.256 0 42.4448-6.8608 85.3504-20.1216 128.256-10.496 33.5872-25.088 67.584-43.8272 102.144h43.3664l10.3936-16.384a436.736 436.736 0 0 0 39.8848-84.2752 409.4464 409.4464 0 0 0 0-259.0208A421.6832 421.6832 0 0 0 696.7296 307.2zM462.8992 460.1344H402.176l81.2544 119.3984-91.8016 135.68h61.2352l59.392-94.208 58.88 94.208h61.2352l-91.8016-135.68 81.2544-119.3984H561.152l-48.896 77.9776-49.3056-77.9776z" fill="currentColor" ></path></symbol><symbol id="icon-loop" viewBox="0 0 1024 1024"><path d="M510.9248 77.7216a440.0128 440.0128 0 0 1 17.1008 0.3584l8.6016 0.4096a436.3776 436.3776 0 0 1 4.6592 0.3072l2.304 0.1536a434.5856 434.5856 0 0 1 27.5968 2.9696l3.4816 0.512 2.7136 0.4096 4.4544 0.7168 1.8432 0.256a430.2336 430.2336 0 0 1 10.4448 1.9456l1.8944 0.4096 3.3792 0.6656 1.024 0.2048a429.1584 429.1584 0 0 1 11.2128 2.5088l1.9456 0.512 3.1232 0.7168a426.752 426.752 0 0 1 24.7808 7.0656l2.4576 0.768 11.008 3.7376 2.4064 0.8704c13.1584 4.7616 26.0608 10.0864 38.6048 16.0768l2.3552 1.1264-2.3552-1.1264A432.2304 432.2304 0 1 1 78.5408 509.952l0.3584 18.0736a432.5888 432.5888 0 0 1 15.6672-135.168c10.8032-38.4 26.7776-74.752 47.104-108.032l6.5024-10.24a434.5856 434.5856 0 0 1 89.088-99.328l3.584-2.8672a304.64 304.64 0 0 1 5.9904-4.7616l3.072-2.304c8.8064-6.7072 17.92-13.1072 27.3408-19.1488l2.7136-1.7408a416 416 0 0 1 63.744-33.28 774.6048 774.6048 0 0 1 12.6976-5.0688l1.5872-0.5632c3.3792-1.28 6.7584-2.56 10.1376-3.6864l2.4064-0.8704a404.5824 404.5824 0 0 1 27.3408-8.3456l6.9632-1.8432a429.056 429.056 0 0 1 2.56-0.6144l2.5088-0.6144a429.2096 429.2096 0 0 1 100.8128-11.8272z m71.8848 403.6096a15.2064 15.2064 0 0 0-3.2768 0.4096l-2.6624 0.9216a14.848 14.848 0 0 0-9.0624 13.8752v83.8656H382.3616c-56.2176 0-84.8896-20.5824-109.056-47.8208-4.352-4.864-12.4416-1.1264-11.52 5.3248 8.3456 58.88 35.6864 91.904 67.1744 110.3872h-0.0512c36.5056 21.504 78.592 23.3472 103.3216 23.3472h135.5264v0.0512h0.0512v85.1968c0 8.9088 7.2704 15.0528 15.0016 15.2064l3.2768-0.3072a15.1552 15.1552 0 0 0 6.4-3.072l161.6896-130.1504a15.2064 15.2064 0 0 0 0-23.7056l-161.6896-130.1504a15.104 15.104 0 0 0-6.4-3.072zM438.784 235.6224l-3.328 0.3072a15.1552 15.1552 0 0 0-6.4 3.072L267.3664 369.152a15.1552 15.1552 0 0 0-2.4064 2.4064l-1.024 1.3824a15.2064 15.2064 0 0 0 3.4304 19.8656l11.9296 9.5744 149.8112 120.5248a15.1552 15.1552 0 0 0 6.4 3.072l3.2768 0.3584 3.2768-0.4608c6.4-1.536 11.7248-7.168 11.7248-14.7968V427.3152h185.4464l10.9056 0.256c49.152 2.56 75.5712 22.1184 98.1504 47.5648a6.4512 6.4512 0 0 0 5.7856 2.1504l1.9456-0.5632a6.4512 6.4512 0 0 0 3.7888-6.912c-10.752-75.4688-52.4288-108.4928-94.208-122.88a199.5776 199.5776 0 0 0-42.496-9.216l1.1776 0.1536a252.2112 252.2112 0 0 0-9.8304-0.9728l8.704 0.8192a257.024 257.024 0 0 0-10.8544-0.9728l2.1504 0.1536a278.8864 278.8864 0 0 0-7.8336-0.512l5.6832 0.3584a288.0512 288.0512 0 0 0-8.0896-0.4096l2.4064 0.1024a315.0848 315.0848 0 0 0-8.4992-0.3072l6.144 0.2048a331.1616 331.1616 0 0 0-8.3968-0.256l-6.5536-0.0512H453.7856V250.8288a15.104 15.104 0 0 0-11.776-14.7968l-3.2256-0.4096z" fill="currentColor" ></path></symbol><symbol id="icon-file-sub" viewBox="0 0 1024 1024"><path d="M592 93.184c-44.16-0.96-98.496-22.528-120.064-48.64-21.824-26.24-74.176-46.72-116.992-44.352-79.04 3.968-157.888 10.88-236.416 20.672C75.776 26.432 35.072 68.032 29.376 112.064a3154.048 3154.048 0 0 0 0 782.592c5.76 43.968 46.4 85.568 89.152 91.136 262.72 32.896 525.44 32.896 788.224 0 42.688-5.568 83.392-47.168 89.088-91.136 28.8-231.04 32.064-462.08 9.856-693.12-4.352-44.032-44.992-84.608-88.832-89.152a4115.072 4115.072 0 0 0-324.928-19.2z" fill="#DEA151" ></path><path d="M995.84 894.592c-5.696 44.032-46.4 85.632-89.088 91.2-262.784 32.896-525.44 32.896-788.224 0-42.752-5.568-83.456-47.168-89.152-91.2A3156.928 3156.928 0 0 1 6.656 402.56c1.472-44.032 41.088-82.304 86.656-84.544 279.424-12.608 559.232-12.608 838.592 0 45.632 2.24 85.248 40.512 86.72 84.544a3155.968 3155.968 0 0 1-22.784 492.032z" fill="#F0CA58" ></path><path d="M143.36 190.528a5356.288 5356.288 0 0 1 745.152 0.448c3.008 41.792 5.376 83.584 7.168 125.44A9285.44 9285.44 0 0 0 136.256 316.16c1.792-41.92 4.096-83.84 7.04-125.632z" fill="#F2F2F2" ></path></symbol><symbol id="icon-reset" viewBox="0 0 1024 1024"><path d="M672.694857 864.694857H256.731429a32.036571 32.036571 0 0 1 0-64h415.963428a192 192 0 0 0 0-384H202.166857l139.629714 139.483429a30.646857 30.646857 0 1 1-43.446857 43.300571L104.009143 405.430857a30.646857 30.646857 0 0 1 0-43.373714l194.340571-194.048a30.646857 30.646857 0 1 1 43.446857 43.373714L200.265143 352.621714h472.502857a256 256 0 0 1 0 512z" fill="#3BA05C" ></path></symbol><symbol id="icon-speak" viewBox="0 0 1024 1024"><path d="M512 608c88 0 160-72 160-160V256c0-88-72-160-160-160A160.448 160.448 0 0 0 352 256v192c0 88 72 160 160 160z m284.608-115.584a31.936 31.936 0 1 0-63.232-9.792A225.472 225.472 0 0 1 512 672a225.536 225.536 0 0 1-221.44-189.44 32 32 0 1 0-63.168 9.728A285.952 285.952 0 0 0 480 734.08V832H384a32 32 0 1 0 0 64h256a32 32 0 1 0 0-64H544v-97.92a285.952 285.952 0 0 0 252.608-241.664z" fill="#979AAB" ></path></symbol><symbol id="icon-a-preview-file" viewBox="0 0 1024 1024"><path d="M648.305778 170.666667c58.026667 0 105.244444 46.876444 105.244444 104.561777v258.161778h-52.053333V274.659556a52.906667 52.906667 0 0 0-52.622222-52.280889H232.675556a52.622222 52.622222 0 0 0-52.622223 52.280889v517.233777a52.906667 52.906667 0 0 0 52.622223 52.280889h260.209777v51.825778H233.244444a104.96 104.96 0 0 1-105.244444-104.561778V275.228444C128 217.543111 175.217778 170.666667 233.244444 170.666667h415.061334z m19.342222 412.558222c77.824 0 148.138667 42.894222 183.808 111.843555a17.294222 17.294222 0 0 1 0 15.701334 206.165333 206.165333 0 0 1-183.864889 111.843555 205.994667 205.994667 0 0 1-183.808-112.014222 16.099556 16.099556 0 0 1 0-15.530667 206.165333 206.165333 0 0 1 183.808-111.843555z m0 42.894222c-61.838222 0-109.283556 23.722667-140.117333 76.743111 30.833778 52.963556 78.279111 76.743111 140.060444 76.743111 61.781333 0 109.226667-23.779556 140.060445-76.8-30.776889-52.963556-78.279111-76.686222-140.060445-76.686222z m-0.170667 42.609778c19.000889 0 34.474667 15.36 34.474667 34.247111a34.417778 34.417778 0 0 1-68.835556 0c0-18.887111 15.473778-34.247111 34.360889-34.247111zM466.944 533.276444a26.168889 26.168889 0 0 1 23.608889 12.572445 26.055111 26.055111 0 0 1-23.608889 39.253333h-156.444444a25.941333 25.941333 0 0 1 0-51.825778h156.444444z m156.273778-155.420444a26.168889 26.168889 0 0 1 23.608889 12.629333 26.055111 26.055111 0 0 1-23.608889 39.253334H310.499556a25.941333 25.941333 0 0 1 0-51.882667h312.718222z" fill="#FFFFFF" ></path></symbol><symbol id="icon-reparse" viewBox="0 0 1024 1024"><path d="M998.765714 523.629714c13.824 0 25.014857 11.190857 25.014857 25.014857a475.282286 475.282286 0 0 1-875.593142 256.219429l-27.574858 55.149714a25.014857 25.014857 0 1 1-44.763428-22.454857l44.178286-88.283428a24.868571 24.868571 0 0 1 26.550857-25.526858 25.014857 25.014857 0 0 1 8.265143 0.804572l99.474285 24.868571a25.014857 25.014857 0 0 1-12.068571 48.566857l-46.372572-11.556571A425.252571 425.252571 0 0 0 973.750857 548.571429c0-13.897143 11.190857-25.014857 25.014857-25.014858zM430.957714 365.714286l6.729143 0.658285c2.633143 0.438857 285.549714 160.109714 285.549714 160.109715 20.114286 17.846857 7.314286 34.523429-6.582857 45.933714-1.828571 1.462857-194.779429 113.078857-249.929143 144.969143l-10.678857 6.217143-3.876571 2.194285c-16.676571 8.923429-39.497143 8.923429-47.250286-11.995428-0.877714-2.194286-2.267429-250.221714-2.56-303.396572L402.285714 400.457143l0.731429-0.512c0.731429-18.651429 8.265143-38.034286 34.669714-33.645714z m-15.945143-273.408a475.282286 475.282286 0 0 1 533.869715 200.045714l27.501714-55.149714a25.014857 25.014857 0 1 1 44.690286 22.454857l-44.105143 88.283428a24.868571 24.868571 0 0 1-26.624 25.526858 24.868571 24.868571 0 0 1-8.192-0.804572l-99.547429-24.868571a25.014857 25.014857 0 0 1 12.068572-48.566857l46.445714 11.629714A425.252571 425.252571 0 0 0 123.245714 548.571429a25.014857 25.014857 0 0 1-50.029714 0 475.282286 475.282286 0 0 1 341.796571-456.265143z" fill="#3BA05C" ></path></symbol><symbol id="icon-testing" viewBox="0 0 1024 1024"><path d="M720.704 525.824a202.432 202.432 0 0 1 158.656 328.128l69.888 69.952a29.952 29.952 0 0 1-41.728 42.88l-0.512-0.576-71.296-71.296A202.432 202.432 0 0 1 518.4 728.32a202.432 202.432 0 0 1 202.304-202.496z m32.192-464.832c62.848 0 113.92 50.496 114.944 113.152v240.832a29.888 29.888 0 0 1-59.776 0.704V176.064a55.232 55.232 0 0 0-54.272-55.232H168.96a55.232 55.232 0 0 0-55.168 54.336V824.96c0 30.208 24.192 54.72 54.272 55.232h303.872a29.888 29.888 0 0 1 0.64 59.776H168.96c-62.848 0-113.92-50.432-114.944-113.152V176.064c0-62.912 50.432-114.048 113.024-115.072h585.856z m-32.192 524.672a142.592 142.592 0 1 0 142.528 142.72c0-78.848-63.808-142.72-142.528-142.72z m-231.04-59.904a29.888 29.888 0 0 1 0.64 59.776H212.608a29.888 29.888 0 0 1-0.64-59.776H489.6z m153.664-233.536a29.888 29.888 0 0 1 0.704 59.84H212.608a29.888 29.888 0 0 1-0.64-59.776h431.36z" fill="#979AAB" ></path></symbol><symbol id="icon-agent" viewBox="0 0 1024 1024"><path d="M512.8704 465.4592l343.4496-197.888a116.5824 116.5824 0 0 0-4.1984-2.6624l-287.0784-166.7072a103.7824 103.7824 0 0 0-103.8336-0.256L173.2096 263.168a71.5264 71.5264 0 0 0-4.1984 2.6112l343.8592 199.68z" fill="#757BF2" ></path><path d="M559.5648 943.7184l3.2768-1.7408 287.9488-165.2736a103.7824 103.7824 0 0 0 52.1216-89.8048l0.8192-331.9808c0-1.6384-0.1024-3.2768-0.1536-4.864l-344.064 198.2464v395.4176h0.0512zM464.4352 547.328L121.2416 347.9552a80.8448 80.8448 0 0 0-0.1536 5.0176l-0.8192 331.9808a103.7824 103.7824 0 0 0 51.712 90.0608l287.0784 166.7072c1.792 1.024 3.584 1.9968 5.4272 2.9184v-397.312h-0.0512z" fill="#6C6CEA" ></path><path d="M121.088 352.9728l-0.8192 331.9808a103.7824 103.7824 0 0 0 51.712 90.0608l93.184 54.1184a587.264 587.264 0 0 0 199.3728-3.6352V547.328L121.2416 347.9552a82.0736 82.0736 0 0 0-0.1536 5.0176z m438.4768 445.6448a583.8336 583.8336 0 0 0 343.9616-351.744l0.256-91.9552c0-1.6384-0.0512-3.2768-0.1536-4.864l-344.064 198.2464v250.3168z" fill="#757BF2" ></path><path d="M121.088 352.9728l-0.6656 271.4624c24.7808 3.1744 49.664 4.7616 74.6496 4.7616 97.2288 0 188.8768-23.8592 269.4144-65.9968v-15.8208L121.2416 347.9552a82.0736 82.0736 0 0 0-0.1536 5.0176z m558.848 16.1792a579.7888 579.7888 0 0 0 74.5984-160.9216l-189.5424-110.08a103.7824 103.7824 0 0 0-103.8336-0.256L173.2096 263.168a71.5264 71.5264 0 0 0-4.1984 2.6112l343.808 199.68L679.936 369.152z" fill="#8486F8" ></path><path d="M564.992 98.1504a103.7824 103.7824 0 0 0-103.8336-0.256L173.2096 263.168a71.5264 71.5264 0 0 0-4.1984 2.6112l153.3952 89.088a584.3456 584.3456 0 0 0 247.1936-254.0032l-4.608-2.7136z" fill="#8D92F8" ></path></symbol><symbol id="icon-chat" viewBox="0 0 1024 1024"><path d="M790.72 242.944h-21.76a108 108 0 0 0-102.176-73.216H197.344A108.032 108.032 0 0 0 89.28 277.76v312.128a99.328 99.328 0 0 0 99.328 99.328c28.416 0 51.52 23.04 51.52 51.52v25.536c0 35.072 41.728 53.376 67.52 29.632l101.248-93.088a51.52 51.52 0 0 1 34.88-13.6h223.008a108.032 108.032 0 0 0 108.032-108.032v-253.568h15.872c32.16 0 58.24 26.176 58.24 58.272v290.656a50.24 50.24 0 0 1-50.144 50.176 90.4 90.4 0 0 0-90.336 90.336v12.736l-86.112-79.2a90.176 90.176 0 0 0-61.12-23.872h-110.496a42.4 42.4 0 0 0 0 84.672h110.496c1.376 0 2.752 0.544 3.776 1.504l94.272 86.688a79.52 79.52 0 0 0 53.76 21.248c11.104 0 22.016-2.304 32.384-6.848a78.784 78.784 0 0 0 47.776-73.12v-23.808c0-3.072 2.496-5.568 5.6-5.6a135.04 135.04 0 0 0 134.912-134.848v-290.656a143.04 143.04 0 0 0-142.976-143.008z m-277.632 309.856H273.984a39.616 39.616 0 1 1 0-79.264h239.104a39.616 39.616 0 1 1 0 79.264z m80.16-154.528H273.984a39.616 39.616 0 1 1 0-79.232h319.264a39.616 39.616 0 0 1 0 79.232z" fill="#6C6CEA" ></path><path d="M622.4 750.592a90.176 90.176 0 0 0-61.12-23.872h-110.56c-23.36 0-42.336 19.008-42.336 42.368 0 21.024 15.456 38.496 35.616 41.76a578.304 578.304 0 0 0 178.688-60l-0.32-0.256z" fill="#757BF2" ></path><path d="M923.808 333.664a143.104 143.104 0 0 0-133.088-90.72h-21.76a108 108 0 0 0-102.176-73.216H197.344A108.032 108.032 0 0 0 89.28 277.76v312.128a99.328 99.328 0 0 0 99.328 99.328c28.416 0 51.52 23.04 51.52 51.52v25.536c0 35.072 41.728 53.376 67.52 29.632l101.248-93.088a51.52 51.52 0 0 1 34.88-13.6h223.008a108.032 108.032 0 0 0 108.032-108.032v-253.568h15.872c32.16 0 58.24 26.176 58.24 58.272v150.4a576.64 576.64 0 0 0 74.88-202.624z m-410.72 219.136H273.984a39.616 39.616 0 1 1 0-79.264h239.104a39.616 39.616 0 1 1 0 79.264z m80.16-154.528H273.984a39.616 39.616 0 1 1 0-79.232h319.264a39.616 39.616 0 1 1 0 79.232z" fill="#757BF2" ></path><path d="M447.296 552.8H273.984a39.616 39.616 0 1 1 0-79.264h239.104c13.76 0 25.92 7.072 33.024 17.728a582.176 582.176 0 0 0 200.192-286.56 107.392 107.392 0 0 0-79.52-35.136H197.344A108.032 108.032 0 0 0 89.28 277.6v312.128c0 4.48 0.416 8.96 0.96 13.312 32.96 5.76 66.784 8.8 101.312 8.8 91.776 0.16 178.56-21.12 255.744-59.04zM273.984 319.072h319.264a39.616 39.616 0 1 1 0 79.264H273.984a39.616 39.616 0 1 1 0-79.264z" fill="#8486F8" ></path><path d="M237.376 373.824a39.616 39.616 0 0 1 36.608-54.72h77.696a585.184 585.184 0 0 0 161.76-149.376H197.312A108.032 108.032 0 0 0 89.28 277.76v126.88a579.2 579.2 0 0 0 148.096-30.816z" fill="#8D92F8" ></path></symbol><symbol id="icon-CSV" viewBox="0 0 1024 1024"><path d="M530.368 0h55.424v95.36c98.88 0.64 197.76-1.408 297.216 0.64a28.672 28.672 0 0 1 26.752 11.648c7.04 8.768 10.432 21.12 9.216 33.536 1.536 234.688 0 469.44 0.896 704.192-0.896 24 1.856 50.688-8.96 72-13.568 12.48-31.04 11.264-46.848 11.264H585.984V1024h-58.112c-141.056-32.64-282.432-63.04-423.744-94.912V95.552C246.208 63.552 388.288 32.512 530.368 0z" fill="#D24625" ></path><path d="M585.792 131.008V893.44h305.728V131.008H585.728z m148.8 144.896c3.968 3.136 8.96 4.672 14.016 4.288a22.464 22.464 0 0 0 13.44-3.584 11.712 11.712 0 0 0 4.864-9.6c-0.256-5.504-3.072-10.24-7.168-12.224a145.984 145.984 0 0 0-14.976-6.336 117.12 117.12 0 0 1-17.984-8.128 22.72 22.72 0 0 1-9.024-19.52 24.384 24.384 0 0 1 8.96-20.16 34.112 34.112 0 0 1 41.728 0c5.632 5.76 8.96 14.336 8.96 23.424h-12.096a22.848 22.848 0 0 0-5.888-12.864 17.664 17.664 0 0 0-12.8-3.968 19.008 19.008 0 0 0-11.072 2.56c-3.072 1.856-4.992 5.888-4.736 10.112 0.128 4.288 2.24 8.128 5.376 9.792 4.224 2.368 8.576 4.352 13.12 6.016 7.04 2.368 13.76 5.568 20.288 9.472 6.144 4.096 9.984 12.16 9.92 20.864a26.688 26.688 0 0 1-8.064 20.544 38.912 38.912 0 0 1-44.992 0.64 36.48 36.48 0 0 1-10.944-27.52h12.608a24.96 24.96 0 0 0 6.4 16.192z m-96.64-32a63.808 63.808 0 0 1 8.96-34.944c6.848-10.496 17.344-16.256 28.16-15.36a30.272 30.272 0 0 1 22.4 8.96c5.44 5.888 8.96 14.272 9.728 23.296h-12.352a23.68 23.68 0 0 0-7.168-14.016 19.648 19.648 0 0 0-13.312-4.288c-7.04-0.512-13.76 3.328-18.048 10.432a50.56 50.56 0 0 0-5.632 25.92 51.712 51.712 0 0 0 5.568 26.368c8.384 11.136 21.888 13.184 32.128 4.864a31.68 31.68 0 0 0 7.68-16.512h12.352a46.144 46.144 0 0 1-12.032 26.496 28.928 28.928 0 0 1-21.44 8.96c-10.88 0.96-21.568-4.672-28.672-15.168a65.088 65.088 0 0 1-8.32-34.944z m187.968 48.256h-15.168l-27.008-96.64h13.696l21.12 79.04 21.12-79.104h13.632l-27.392 96.704z" fill="#FFFFFF" ></path><path d="M618.24 366.144h240.832v110.336H618.24V366.08z m0 164.736h240.832v110.4H618.24V530.88z m0 164.8h240.832v110.336H618.24V695.68z" fill="#D24625" ></path><path d="M408 584.256l-2.56 12.608c-3.2 20.032-11.52 38.208-23.68 51.712a57.92 57.92 0 0 1-41.6 15.168c-26.496 0-45.952-10.048-58.112-29.824-12.16-19.84-17.536-48.576-17.536-86.72 0-38.208 6.016-66.112 17.92-85.952a62.336 62.336 0 0 1 56.448-31.488 60.8 60.8 0 0 1 41.408 13.056c10.944 10.048 18.432 24.96 21.12 41.6l2.688 12.48h71.872L473.6 477.504c-3.648-37.824-18.752-72.32-42.24-96.576-26.176-25.344-58.624-38.272-91.584-36.608-49.664 0-88.128 21.248-114.304 62.848-22.4 35.648-34.496 82.88-34.496 140.032 0 57.152 11.2 104.768 33.152 139.328 25.92 41.472 65.088 62.464 116.864 62.464 31.488 0.768 62.08-12.16 86.848-36.608 26.496-27.2 44.032-65.664 49.344-108.032l3.072-20.16H408z" fill="#FFFFFF" ></path></symbol><symbol id="icon-data" viewBox="0 0 1024 1024"><path d="M148.576 419.488l345.344 160.16a67.712 67.712 0 0 0 58.624-0.672l336.864-165.472a67.744 67.744 0 0 0 38.016-60.672 67.712 67.712 0 0 0-37.248-61.12L556.48 122.432a68.32 68.32 0 0 0-61.28-0.16L146.72 296.864a67.616 67.616 0 0 0-37.568 61.888 67.52 67.52 0 0 0 39.424 60.736z m738.208 220.416L522.912 828.16 149.76 645.952a43.456 43.456 0 1 0-38.112 78.176l375.232 183.232a82.24 82.24 0 0 0 73.888-0.864l366.016-189.344a43.456 43.456 0 0 0 18.656-58.624 43.52 43.52 0 0 0-58.688-18.624z" fill="#FC7032" ></path><path d="M111.616 574.08l375.232 183.232a82.24 82.24 0 0 0 73.92-0.896l366.016-189.312a43.456 43.456 0 1 0-40-77.28L522.912 678.08 149.76 495.872a43.456 43.456 0 0 0-58.144 20.032 43.424 43.424 0 0 0 19.968 58.144z" fill="#FC7032" ></path><path d="M495.264 122.304L146.752 296.864A67.616 67.616 0 0 0 109.12 358.72c0.416 26.368 15.52 49.6 39.424 60.736l345.344 160.16a67.712 67.712 0 0 0 58.624-0.672l253.664-124.64a523.264 523.264 0 0 0 1.6-204.384l-251.36-127.488a68.096 68.096 0 0 0-61.184-0.16z" fill="#FF7C33" ></path><path d="M778.048 546.112l-255.2 131.968-373.024-182.208a43.456 43.456 0 1 0-38.144 78.176l375.232 183.264a82.24 82.24 0 0 0 73.888-0.896l127.936-66.176a519.552 519.552 0 0 0 89.28-144.128z m-266.88 276.32L149.824 645.952a43.456 43.456 0 1 0-38.144 78.176l278.72 136.096a516.48 516.48 0 0 0 120.768-37.792z" fill="#FF7C33" ></path><path d="M674.144 182.176L556.48 122.528a68.32 68.32 0 0 0-61.28-0.16L146.72 296.864a67.616 67.616 0 0 0-37.568 61.888c0.416 26.368 15.52 49.6 39.424 60.736l328 152.096a519.776 519.776 0 0 0 197.568-389.44z m-270.336 437.76l-253.984-124.064a43.456 43.456 0 1 0-38.144 78.176l184.256 90.016a518.464 518.464 0 0 0 107.872-44.16zM153.664 683.84c22.08 0 43.776-1.536 65.152-4.192l-68.992-33.696a43.456 43.456 0 0 0-62.208 33.632 540.8 540.8 0 0 0 66.048 4.256z" fill="#FF9552" ></path><path d="M495.264 122.304L146.752 296.864A67.616 67.616 0 0 0 109.12 358.72c0.416 26.368 15.52 49.6 39.424 60.736l80.608 37.376A522.112 522.112 0 0 0 525.376 115.2a68.096 68.096 0 0 0-30.112 7.104z" fill="#FFA56A" ></path></symbol><symbol id="icon-doc" viewBox="0 0 1024 1024"><path d="M894.08 863.616H525.44c-14.336 0-25.92-14.08-25.92-31.36V193.92c0-17.344 11.584-31.424 25.856-31.424h368.64c14.272 0 25.856 14.08 25.856 31.36v638.272c0 17.344-11.584 31.36-25.856 31.36v0.064z" fill="#E8E8E8" ></path><path d="M788.672 353.28H525.44c-14.272 0-25.856-14.08-25.856-31.424s11.584-31.424 25.856-31.424h263.296c14.336 0 25.856 14.08 25.856 31.36 0 16.32-11.52 31.424-25.856 31.424v0.064z m0 127.808H525.44c-14.272 0-25.856-14.08-25.856-31.36 0-17.344 11.584-31.488 25.856-31.488h263.296c14.336 0 25.856 14.08 25.856 31.424 0 17.408-11.52 31.424-25.856 31.424z m0 126.848H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z m0 127.872H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z" fill="#B2B2B2" ></path><path d="M595.008 1024l-490.88-113.792V113.792L595.008 0z" fill="#0D47A1" ></path><path d="M138.24 409.6v204.8h58.432c25.92 0 45.44-9.152 58.88-27.52 12.736-17.472 19.2-42.432 19.2-74.88 0-32.64-6.464-57.664-19.2-74.88-13.44-18.368-32.96-27.52-58.88-27.52H138.24z m26.24 28.672h27.264c19.904 0 34.432 5.76 43.648 17.536 8.96 11.456 13.44 30.336 13.44 56.192 0 25.28-4.48 43.904-13.44 55.936-9.216 11.776-23.68 17.728-43.648 17.728h-27.328V438.272zM342.848 409.6c-21.568 0-38.464 9.6-50.688 29.504-11.776 18.816-17.472 43.072-17.472 73.152 0 29.824 5.76 54.144 17.472 72.896 12.16 19.328 29.12 29.248 50.688 29.248 21.44 0 38.336-9.6 50.688-28.992 11.776-18.432 17.728-42.752 17.728-73.152 0-30.336-5.952-54.912-17.728-73.344-12.352-19.648-29.248-29.312-50.688-29.312z m0 28.416c14.592 0 25.792 6.4 33.6 19.584 7.68 13.248 11.648 31.488 11.648 54.656 0 23.232-3.968 41.152-11.648 54.144a37.312 37.312 0 0 1-33.6 19.584c-14.528 0-25.92-6.912-33.792-20.48-7.68-13.184-11.392-30.848-11.392-53.184 0-22.656 3.776-40.32 11.392-53.632 8.064-13.76 19.264-20.672 33.792-20.672zM482.56 409.6a59.904 59.904 0 0 0-54.656 31.168c-11.072 18.24-16.512 42.24-16.512 71.488 0 29.824 5.184 53.568 15.872 71.232 12.16 20.48 30.784 30.912 55.68 30.912a55.104 55.104 0 0 0 41.536-18.24c12.352-12.928 20.096-30.848 23.424-54.08h-23.872c-2.944 14.912-8 26.24-15.104 33.664a35.968 35.968 0 0 1-26.176 10.24 39.296 39.296 0 0 1-36.16-20.16c-7.168-12.416-10.688-30.4-10.688-53.568 0-22.592 3.52-40.32 10.88-52.928a37.888 37.888 0 0 1 35.392-21.312 36.48 36.48 0 0 1 25.728 8.832c6.912 6.08 11.52 15.424 14.08 28.48h23.808c-2.304-19.904-8.768-35.904-19.648-47.488a56.96 56.96 0 0 0-43.52-18.24z" fill="#FFFFFF" ></path></symbol><symbol id="icon-excel" viewBox="0 0 1024 1024"><path d="M530.752 0h55.04v95.296c92.8 0 185.6 0.192 278.272-0.384 15.68 0.832 32.896-0.64 46.528 11.392 9.6 17.6 8.384 40.064 9.152 60.288-0.512 206.72-0.576 413.376-0.256 619.968-0.384 34.56 2.56 69.952-3.136 104.128-3.712 24.704-26.88 25.344-42.432 26.176-96 0.384-192.064-0.256-288.192 0V1024h-57.472c-141.248-32.96-282.752-63.488-424.128-95.296V95.36C246.336 63.552 388.608 32.192 530.752 0z" fill="#207245" ></path><path d="M585.856 130.944h305.792v750.208H585.792v-71.488h74.176v-83.328H585.856v-47.616h74.112V595.328H585.856v-47.616h74.112V464.384H585.856v-47.68h74.112V333.44H585.856v-47.616h74.112V202.368H585.856z" fill="#FFFFFF" ></path><path d="M696.96 202.432h129.728v83.328h-129.664V202.368z m0 130.944h129.728v83.328h-129.664V333.44z m0 131.008h129.728v83.328h-129.664V464.384z m0 130.944h129.728v83.392h-129.664V595.328z m0 131.008h129.728v83.392h-129.664v-83.392z" fill="#207245" ></path><path d="M137.216 484.608V576H204.16v-9.088h-56.448v-33.536h51.2v-9.088h-51.2v-30.592h54.272v-9.088H137.216z m75.392 0l31.232 43.904L210.176 576h12.928l27.136-39.168 27.264 39.168h12.928l-33.92-47.488 31.488-43.904h-12.928l-24.832 35.584-24.704-35.584h-12.928z m127.104-1.792c-14.08 0-25.088 4.736-32.768 14.464a51.392 51.392 0 0 0-10.496 33.408c0 13.568 3.328 24.576 10.24 33.024 7.552 9.344 18.432 14.08 32.64 14.08a39.872 39.872 0 0 0 25.216-8.32 39.68 39.68 0 0 0 14.336-24.832h-10.112a30.208 30.208 0 0 1-10.88 18.048 29.952 29.952 0 0 1-18.56 5.76 29.44 29.44 0 0 1-24.448-10.624c-5.376-6.656-7.936-15.744-7.936-27.136s2.688-20.48 8.064-27.392a29.248 29.248 0 0 1 24.576-11.136c7.168 0 13.184 1.664 18.176 5.12 5.12 3.584 8.448 8.704 9.728 15.36H377.6a32.192 32.192 0 0 0-12.288-21.76 40.512 40.512 0 0 0-25.6-8.064z m53.76 1.792V576h66.944v-9.088h-56.448v-33.536h51.2v-9.088h-51.2v-30.592h54.272v-9.088H393.472z m81.536 0V576h63.232v-9.088h-52.864V484.608h-10.368z" fill="#FFFFFF" ></path></symbol><symbol id="icon-md" viewBox="0 0 1024 1024"><path d="M533.376 0h52.544v107.2c99.072 0.64 198.208-1.344 297.216 0.576 21.312-2.624 38.016 18.688 35.84 46.08 1.6 222.4-0.32 444.928 0.896 667.52-0.896 24.064 1.856 50.688-8.832 72-13.568 12.608-31.168 10.944-47.04 11.968-92.672-0.64-185.344-0.384-278.08-0.384V1024h-57.664c-141.184-33.088-282.752-63.36-424.064-95.232-0.128-277.824-0.128-555.584 0-833.28C247.168 63.68 390.144 31.296 533.376 0z" fill="#20B2AA" ></path><path d="M582.848 142.912v354.176h115.84V512h-115.84v104.192h242.496v44.608h-242.56v56.576h242.56v44.672h-242.56v107.072h305.92V142.912h-305.92z m180.8 293.12a98.624 98.624 0 0 0-11.072 36.48c-0.256 3.584-2.112 6.72-4.736 8.064l-40 18.56-4.48-3.008 18.432-31.744c1.408 0.64 3.008 0.96 4.544 0.896 7.168-0.064 12.928-6.976 12.8-15.36 0-8.32-5.824-15.104-12.992-14.976-7.168 0.064-12.928 6.976-12.864 15.36 0 1.92 0.32 3.712 0.896 5.44a11.648 11.648 0 0 1-0.512 9.536l-15.616 26.944-3.712-4.48-0.128-49.792c0-3.776 1.6-7.168 4.224-8.832 6.4-3.84 18.432-12.608 27.648-26.752 2.496-3.712 6.72-4.416 9.856-1.664l25.6 21.568a10.24 10.24 0 0 1 3.264 6.336c0.448 2.56 0 5.12-1.152 7.424z m18.176-17.28c-2.368 4.032-6.72 4.992-10.048 2.368l-39.04-31.232a10.112 10.112 0 0 1-3.392-6.208 12.16 12.16 0 0 1 1.152-7.552l0.256-0.512c2.304-4.48 4.096-1.92 7.616 0.96l38.656 31.808c3.712 3.072 7.488 5.888 4.736 10.432h0.064z m33.024-62.912l-22.016 41.856c-3.648 6.912-11.008 8.768-16.384 4.096l-26.112-22.592c-5.376-4.736-6.784-14.144-3.2-21.12l22.016-41.856a12.096 12.096 0 0 1 7.616-6.4 9.792 9.792 0 0 1 8.832 2.304l26.048 22.656c5.376 4.736 6.848 14.08 3.2 21.12V355.84z" fill="#FFFFFF" ></path></symbol><symbol id="icon-jpg" viewBox="0 0 1024 1024"><path d="M533.312 0h52.544v107.136c99.072 0.64 198.272-1.28 297.216 0.576 21.312-2.56 38.016 18.752 35.84 46.08 1.6 222.464-0.256 444.992 0.96 667.52-0.896 24.064 1.856 50.752-8.96 72.064-13.44 12.608-31.104 11.008-46.976 11.968-92.672-0.64-185.344-0.384-278.08-0.384V1024h-57.664c-141.248-33.088-282.688-63.36-424-95.36-0.128-277.76-0.128-555.456 0-833.152C247.04 63.744 390.208 31.36 533.312 0z" fill="#077467" ></path><path d="M582.784 142.784v515.136l6.912-4.48 12.8-7.488 12.736-19.328h25.472l41.728-64 72.96 101.248h26.688l56.704 56.576-60.16-32.768h-22.08l-67.2-53.504-35.84 55.04-17.408-26.88-53.312-4.352v211.328h305.792V142.784H582.784z m206.144 287.36c-23.04 0-41.664-24-41.664-53.632 0-29.632 18.624-53.632 41.664-53.632 23.104 0 41.792 24 41.792 53.632v0.064c-0.128 29.632-18.688 53.632-41.792 53.568z m-599.808-16h23.168V508.16c0 12.288-0.768 21.632-2.56 28.288a40.96 40.96 0 0 1-12.224 20.864 33.92 33.92 0 0 1-23.488 7.808 31.04 31.04 0 0 1-26.24-12.288c-6.144-8.192-9.28-20.224-9.344-36.032l21.952-3.264c0.256 8.512 1.28 14.528 2.88 18.048 2.56 5.376 6.4 7.936 11.52 7.936 5.184 0 8.832-1.92 11.008-5.76 2.24-3.84 3.2-11.712 3.2-23.744V414.144h0.128z m48 148.288V414.08h37.376c14.144 0 23.36 0.768 27.648 2.304a30.72 30.72 0 0 1 16.64 14.464c4.416 7.552 6.592 17.088 6.592 28.928a60.16 60.16 0 0 1-3.84 22.912 38.592 38.592 0 0 1-9.792 14.72 31.168 31.168 0 0 1-12.16 7.04c-7.936 1.6-16 2.304-24.128 2.048h-15.232v55.936h-23.104v-0.064z m23.232-123.2v42.048h12.8c9.152 0 15.232-0.832 18.368-2.368a16.384 16.384 0 0 0 7.296-7.232 24.32 24.32 0 0 0 2.56-11.52 22.72 22.72 0 0 0-3.712-13.312 15.168 15.168 0 0 0-9.344-6.592 84.416 84.416 0 0 0-16.768-1.024h-11.2z m137.92 68.736v-24.96h50.176v59.008a63.744 63.744 0 0 1-21.184 16.064 63.36 63.36 0 0 1-28.16 6.912 52.288 52.288 0 0 1-31.552-9.728 58.88 58.88 0 0 1-20.224-27.84 111.04 111.04 0 0 1-6.72-39.488c0-15.488 2.56-29.12 7.552-41.216a59.52 59.52 0 0 1 22.144-27.648 48.768 48.768 0 0 1 27.648-7.36c14.272 0 25.472 3.84 33.472 11.584 8.064 7.68 13.248 18.368 15.616 32l-23.168 5.632a32.384 32.384 0 0 0-9.216-17.28 23.68 23.68 0 0 0-16.704-6.272 28.48 28.48 0 0 0-24.32 12.416c-6.08 8.32-9.024 20.608-9.024 36.864 0 17.536 2.944 30.72 9.088 39.552a27.904 27.904 0 0 0 23.936 13.248 32.512 32.512 0 0 0 14.72-3.776 50.944 50.944 0 0 0 12.608-8.896v-18.816h-26.688z" fill="#FFFFFF" ></path></symbol><symbol id="icon-pdf" viewBox="0 0 1024 1024"><path d="M533.312 0h52.544v107.2c99.072 0.64 198.208-1.344 297.216 0.576 21.248-2.624 37.952 18.688 35.84 46.08 1.6 222.4-0.384 444.928 0.896 667.52-0.896 24 1.856 50.688-8.896 72-13.504 12.608-31.104 10.88-46.976 11.904-92.672-0.64-185.344-0.32-278.08-0.32V1024h-57.6c-141.312-33.088-282.816-63.36-424.128-95.232V95.488C247.168 63.68 390.144 31.36 533.312 0z" fill="#F2554E" ></path><path d="M156.16 555.392v-156.16h34.304c12.928 0 21.44 0.768 25.344 2.304a30.08 30.08 0 0 1 15.232 15.36c4.096 7.808 6.144 17.92 6.144 30.336 0 9.6-1.216 17.664-3.584 24.192a42.24 42.24 0 0 1-8.96 15.424 27.52 27.52 0 0 1-11.072 7.36 83.2 83.2 0 0 1-22.144 2.24h-13.888v58.88H156.16z m21.376-129.792v44.352h11.648c8.384 0 14.016-0.832 16.832-2.432a16.512 16.512 0 0 0 6.656-7.68 28.8 28.8 0 0 0 2.432-12.16 26.624 26.624 0 0 0-3.392-14.08 14.464 14.464 0 0 0-8.512-6.912c-5.12-0.896-10.24-1.28-15.36-1.024h-10.304z m76.992-26.432h39.04c8.832 0 15.488 1.024 20.16 3.008 6.144 2.688 11.648 7.68 15.872 14.336 4.608 7.36 8.064 16 10.112 25.28 2.304 10.048 3.456 22.336 3.456 36.992 0 12.8-1.088 23.872-3.2 33.216-2.688 11.392-6.464 20.544-11.328 27.584a36.8 36.8 0 0 1-15.04 12.48 44.8 44.8 0 0 1-18.88 3.328h-40.192v-156.16z m21.376 26.368v103.552h15.936a40.32 40.32 0 0 0 12.928-1.536c3.456-1.28 6.4-3.52 8.576-6.464a39.36 39.36 0 0 0 5.568-15.104c1.472-6.976 2.176-16.512 2.176-28.608 0-12.032-0.64-21.312-2.176-27.776a41.216 41.216 0 0 0-6.016-15.104 19.136 19.136 0 0 0-9.92-7.296 64.576 64.576 0 0 0-17.472-1.536l-9.6-0.064z m85.504 129.856v-156.16h72.448v26.304H382.72v36.992h44.16v26.368h-44.16v66.496h-21.312z m454.272 51.392a22.656 22.656 0 0 0-2.752-3.2c-4.096-3.968-12.48-9.792-28.928-14.784a139.008 139.008 0 0 0-5.12-1.472c10.944 10.496 20.288 16.96 27.968 19.2 4.544 1.472 7.36 0.896 8.832 0.256zM721.92 394.88c2.112-9.6 3.584-26.88 0.448-30.4v0.064c-4.16 7.36-9.408 37.504-7.68 55.68 3.264-7.488 4.352-12.48 7.168-25.344z m0.512 161.792a270.72 270.72 0 0 1-23.296-59.456 604.48 604.48 0 0 1-39.68 61.568c20.864-2.24 41.92-2.944 62.976-2.176z" fill="#FFFFFF" ></path><path d="M590.08 156.16v424.96c8.32-3.2 14.016-4.8 14.016-4.8 4.928-6.912 4.352-6.272 10.304-14.784 18.88-27.328 36.48-56.128 52.672-86.4-0.192-2.688-5.888-106.88 18.816-120.768a12.608 12.608 0 0 1 10.24-1.024 19.456 19.456 0 0 1 10.944 9.92c5.568 11.84 5.504 29.12 0.896 53.248-7.296 38.208-18.368 59.52-18.368 59.52s3.52 47.168 36.864 99.584c15.36 1.664 28.544 4.288 39.232 7.68 18.368 5.888 29.312 14.08 33.472 24.896a34.816 34.816 0 0 1-0.192 22.72c-2.752 8.96-5.568 13.376-10.688 16-4.288 2.176-9.664 2.368-15.936 0.32-18.752-6.016-43.52-29.824-59.712-49.024a597.568 597.568 0 0 0-94.464-0.896 508.928 508.928 0 0 1-28.16 37.376v250.432h295.04V156.16H590.208z" fill="#FFFFFF" ></path></symbol><symbol id="icon-png" viewBox="0 0 1024 1024"><path d="M586.24 141.952h305.792v726.656H586.24z" fill="#FFFFFF" ></path><path d="M918.912 153.92a47.424 47.424 0 0 0-9.088-33.664 28.928 28.928 0 0 0-26.752-11.84c-98.944-1.92-198.144 0-297.408 0V0h-52.352C390.144 31.36 247.04 63.744 104.064 95.552v833.6c141.44 32 282.88 62.208 424.128 94.848h57.6v-118.656h278.144c15.936-0.96 33.472 0.64 47.04-12.032 10.688-21.312 7.936-48.192 8.96-72.32-1.408-221.888 0.64-444.544-1.024-667.008z m-27.2 715.648h-305.92V143.04h305.92v726.528z" fill="#0071C5" ></path><path d="M658.368 244.16c19.84 0 30.08 10.624 30.08 32 0 21.248-10.24 32.128-30.4 32.128h-20.48v39.872H624V244.16h34.304z m-20.8 49.536h19.776a18.752 18.752 0 0 0 12.8-4.544 18.56 18.56 0 0 0 4.16-13.504 17.536 17.536 0 0 0-4.288-13.248 17.92 17.92 0 0 0-13.056-4.096h-19.392v35.392z m75.968-49.536l40.96 74.944v-74.88h13.696V348.16h-12.8l-41.344-76.032V348.16h-14.016V244.16h13.568z m132.352 6.912a42.24 42.24 0 0 1 11.136 26.048h-13.632a24.704 24.704 0 0 0-7.488-14.976 21.888 21.888 0 0 0-15.104-4.928 20.8 20.8 0 0 0-18.432 9.856 50.048 50.048 0 0 0-7.616 29.504c-0.512 10.24 1.856 20.48 6.72 28.672 5.824 7.552 14.08 11.392 22.4 10.496a33.472 33.472 0 0 0 11.136-1.664 37.056 37.056 0 0 0 8.896-4.8v-21.952h-21.952v-14.464h35.52v44.416a47.424 47.424 0 0 1-15.104 9.92 55.424 55.424 0 0 1-20.032 3.456 35.008 35.008 0 0 1-30.72-15.744 64.192 64.192 0 0 1-10.24-37.76c-0.512-14.08 3.2-27.712 10.24-38.464a33.088 33.088 0 0 1 28.8-16.064 33.856 33.856 0 0 1 25.472 8.448z m-204.032 405.504h196.608v147.776h-196.608v-147.84z" fill="#0071C5" ></path><path d="M249.92 341.12c50.304 3.072 111.36-25.6 153.472 22.784 39.488 62.464 28.928 177.024-30.72 214.4-20.992 13.824-45.056 11.968-67.968 10.88v139.776l-54.912-6.144c-0.768-127.104-0.96-254.464 0.128-381.76z" fill="#FFFFFF" ></path><path d="M304.96 405.504c18.24-0.96 40.832-5.312 53.248 16.704 10.048 24.448 10.496 53.376 1.152 78.208-10.56 24.128-34.432 22.08-52.864 24.96a2089.408 2089.408 0 0 1-1.536-119.872z m533.952-3.84H641.28v204.8h197.632v-204.8z m-181.312 19.968h165.12v124.8l-24.896-31.936a15.232 15.232 0 0 0-5.888-4.928c-4.288-1.536-13.312 9.408-27.392 33.024-17.728-22.784-56.064-71.168-64-69.76-2.24 0-5.632 2.176-10.304 7.232l-32.64 36.16V421.632z" fill="#0071C5" ></path><path d="M778.24 472.96c4.352 5.12 10.688 6.528 16.128 3.648 5.44-2.88 9.024-9.6 9.152-17.024a20.928 20.928 0 0 0-4.288-12.928 13.248 13.248 0 0 0-10.24-5.376c-6.016 0.128-11.328 4.608-13.632 11.52a22.528 22.528 0 0 0 2.944 20.16z" fill="#0071C5" ></path></symbol><symbol id="icon-text" viewBox="0 0 1024 1024"><path d="M919.872 140.8c0-12.8-2.56-25.6-10.24-32a32.896 32.896 0 0 0-25.472-12.8c-99.392-3.2-198.784 0-298.24 0V0h-56.064C387.072 32 246.848 64 104.064 96v832c140.224 32 283.008 60.8 423.232 96h58.624v-96h277.824c15.36 0 33.216 0 45.952-12.8 10.24-22.4 7.68-48 10.24-73.6V140.8zM496.704 358.4v83.2H376.896v304H292.736V441.6H175.424V336H496.64v22.4z m361.984 483.2H585.92V192h272.768v649.6z" fill="#7A85B0" ></path><path d="M647.936 325.76h148.352v80.704h-148.352v-80.64z m0 164.608h148.352v80.704h-148.352v-80.64z m0 158.144h148.352v80.704h-148.352v-80.64z" fill="#7A85B0" ></path></symbol><symbol id="icon-ppt" viewBox="0 0 1024 1024"><path d="M536.32 70.528h57.408v91.072c108.16 0.64 216.512-1.088 324.672 0.512a35.072 35.072 0 0 1 39.168 39.168c1.728 189.12-0.448 378.368 0.96 567.552-0.96 20.48 2.048 43.136-9.728 61.248-14.72 10.752-33.92 9.344-51.264 10.176-101.248-0.512-202.496-0.32-303.872-0.32v101.248h-62.976c-154.24-28.16-308.864-53.888-463.232-81.024-0.128-236.16-0.128-472.32 0-708.48 156.16-27.008 312.448-54.592 468.864-81.152z" fill="#D24625" ></path><path d="M593.728 192h334.08v617.6h-334.08v-81.024h242.944v-40.512H593.728v-50.56h242.944v-40.576H593.792l-0.192-59.52c40.064 12.48 85.76 12.16 121.6-11.968 38.784-23.04 59.008-66.816 62.336-110.592-44.416-0.256-88.896-0.192-133.184-0.192-0.128-43.968 0.448-88.064-0.96-131.968a2496 2496 0 0 0-49.664 10.24V192z" fill="#FFFFFF" ></path><path d="M664.64 261.568c70.4 3.2 129.536 62.464 133.248 132.608-44.416 0.512-88.896 0.32-133.312 0.32 0-44.352-0.128-88.704 0.064-132.928z m-374.656 131.2c19.968-0.896 44.672-4.608 58.112 14.144 11.52 19.84 10.88 46.016 1.28 66.432-11.52 20.864-37.632 18.816-57.792 21.248a1405.44 1405.44 0 0 1-1.6-101.824z" fill="#D24625" ></path><path d="M114.304 544.832V421.12h37.12c14.08 0 23.232 0.64 27.52 1.856a29.952 29.952 0 0 1 16.512 12.16c4.416 6.144 6.656 14.208 6.656 24.064 0 7.552-1.28 13.952-3.84 19.136a33.28 33.28 0 0 1-21.76 18.048 123.712 123.712 0 0 1-24 1.792h-15.04v46.72h-23.168z m23.168-102.848v35.136h12.672c9.088 0 15.168-0.64 18.24-1.92a15.36 15.36 0 0 0 7.232-6.08 17.6 17.6 0 0 0 2.624-9.6 16.768 16.768 0 0 0-3.648-11.2 15.68 15.68 0 0 0-9.344-5.504 105.216 105.216 0 0 0-16.64-0.832h-11.136z m83.584 102.848V421.12h37.12c14.08 0 23.232 0.64 27.52 1.856a29.952 29.952 0 0 1 16.448 12.16c4.48 6.144 6.656 14.208 6.656 24.064 0 7.552-1.28 13.952-3.84 19.136a33.28 33.28 0 0 1-21.76 18.048 123.712 123.712 0 0 1-23.936 1.792h-15.104v46.72h-23.04z m23.104-102.848v35.136h12.672c9.152 0 15.232-0.64 18.304-1.92a15.36 15.36 0 0 0 7.232-6.08 17.6 17.6 0 0 0 2.56-9.6 16.768 16.768 0 0 0-3.648-11.2 15.68 15.68 0 0 0-9.28-5.504 105.216 105.216 0 0 0-16.64-0.832h-11.2z m109.44 102.848V441.984h-34.048V421.12h91.008v20.864h-33.92v102.784l-23.04 0.064z" fill="#FFFFFF" ></path></symbol><symbol id="icon-search" viewBox="0 0 1024 1024"><path d="M380.928 497.28a117.568 117.568 0 1 0 235.104 0 117.568 117.568 0 0 0-235.104 0z" fill="#1DCE75" ></path><path d="M750.016 121.792H275.296a150.496 150.496 0 0 0-150.304 150.336V752.64a150.496 150.496 0 0 0 150.304 150.336h474.72a150.496 150.496 0 0 0 150.336-150.336V272.128a150.496 150.496 0 0 0-150.336-150.336z m-17.536 611.392a30.368 30.368 0 0 1-42.912-0.256l-79.584-80.64a189.696 189.696 0 0 1-111.488 36.032 191.296 191.296 0 0 1-191.104-191.04 191.296 191.296 0 0 1 191.104-191.104 191.296 191.296 0 0 1 191.04 191.072c0 41.92-13.6 80.704-36.544 112.224l79.776 80.8a30.4 30.4 0 0 1-0.32 42.912z" fill="#05C46D" ></path><path d="M750.016 121.792H275.296a150.496 150.496 0 0 0-150.304 150.336V752.64a150.496 150.496 0 0 0 150.304 150.336h81.28a612.48 612.48 0 0 0 344.608-162.88 29.92 29.92 0 0 1-11.616-7.232l-79.584-80.64a189.696 189.696 0 0 1-111.488 36.064 191.296 191.296 0 0 1-191.104-191.072 191.232 191.232 0 0 1 191.104-191.04 191.296 191.296 0 0 1 191.04 191.072c0 41.92-13.6 80.704-36.544 112.224l79.776 80.8a30.08 30.08 0 0 1 6.656 10.336 612.352 612.352 0 0 0 154.56-407.488c0-26.048-1.6-52.032-4.896-77.888a150.464 150.464 0 0 0-139.072-93.44z" fill="#1DCE75" ></path><path d="M602.976 443.584a117.6 117.6 0 0 0-104.48-63.872 117.696 117.696 0 0 0-117.568 117.536 117.44 117.44 0 0 0 55.136 99.424 619.776 619.776 0 0 0 166.912-153.088z" fill="#3CD38E" ></path><path d="M124.928 272.128v417.184a612.16 612.16 0 0 0 240.736-54.848 190.592 190.592 0 0 1-58.304-137.216 191.296 191.296 0 0 1 191.072-191.072c59.712 0 113.088 27.52 148.16 70.592a610.56 610.56 0 0 0 77.44-254.976H275.296c-82.88 0-150.368 67.456-150.368 150.336z" fill="#3CD38E" ></path><path d="M124.928 272.128v176.48A616.288 616.288 0 0 0 511.552 121.792H275.296c-82.88 0-150.368 67.456-150.368 150.336z" fill="#48E1AA" ></path></symbol><symbol id="icon-system" viewBox="0 0 1024 1024"><path d="M764.416 97.472H259.648a106.752 106.752 0 0 0-106.752 106.752v664a54.72 54.72 0 0 0 78.08 49.536l235.104-111.008a106.688 106.688 0 0 1 91.04-0.032l235.904 111.104a54.72 54.72 0 0 0 78.048-49.536V204.224a106.656 106.656 0 0 0-106.656-106.752zM671.68 577.856h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 1 1 0 76.288z m0-174.144h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 0 1 0 76.288z" fill="#2595E8" ></path><path d="M871.104 469.824v-265.6a106.752 106.752 0 0 0-106.752-106.752H259.648a106.752 106.752 0 0 0-106.752 106.752V838.4a635.296 635.296 0 0 0 229.824 7.68l83.36-39.36a106.784 106.784 0 0 1 74.88-5.952 635.84 635.84 0 0 0 330.144-330.944z m-199.424 108.032h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 1 1 0 76.288z m0-174.144h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 0 1 0 76.288z" fill="#3A9CED" ></path><path d="M362.592 577.856h-10.272a38.144 38.144 0 0 1 0-76.288h142.848a637.12 637.12 0 0 0 103.808-97.856H352.32a38.144 38.144 0 1 1 0-76.288h302.208a630.496 630.496 0 0 0 86.272-229.888H259.648a106.752 106.752 0 0 0-106.752 106.752v422.496a631.072 631.072 0 0 0 209.696-48.96z" fill="#59ADF8" ></path><path d="M498.496 97.472H259.648a106.752 106.752 0 0 0-106.752 106.752v168.064a635.488 635.488 0 0 0 345.6-274.816z" fill="#6BC2FC" ></path></symbol><symbol id="icon-word" viewBox="0 0 1024 1024"><path d="M894.08 863.616H525.44c-14.336 0-25.92-14.08-25.92-31.36V193.92c0-17.344 11.584-31.424 25.856-31.424h368.64c14.272 0 25.856 14.08 25.856 31.36v638.272c0 17.344-11.584 31.36-25.856 31.36v0.064z" fill="#E8E8E8" ></path><path d="M788.672 353.28H525.44c-14.272 0-25.856-14.08-25.856-31.424s11.584-31.424 25.856-31.424h263.296c14.336 0 25.856 14.08 25.856 31.36 0 16.32-11.52 31.424-25.856 31.424v0.064z m0 127.808H525.44c-14.272 0-25.856-14.08-25.856-31.36 0-17.344 11.584-31.488 25.856-31.488h263.296c14.336 0 25.856 14.08 25.856 31.424 0 17.408-11.52 31.424-25.856 31.424z m0 126.848H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z m0 127.872H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z" fill="#B2B2B2" ></path><path d="M595.008 1024l-490.88-113.792V113.792L595.008 0z" fill="#0D47A1" ></path><path d="M455.808 707.584h-62.464l-41.152-250.24a236.8 236.8 0 0 1-3.52-43.392h-0.896a413.44 413.44 0 0 1-4.48 43.328l-42.88 250.304H235.328L170.24 317.504h61.568l34.816 260.096c1.792 10.816 2.688 25.984 3.584 44.352h0.896c0-14.08 2.688-29.248 5.376-45.44l44.608-259.008h59.776l41.088 262.208a371.2 371.2 0 0 1 3.584 42.24h0.896c0.896-14.08 1.792-28.16 3.52-43.328l34.816-260.032h56.256l-65.152 388.992z" fill="#FFFFFF" ></path></symbol><symbol id="icon-file" viewBox="0 0 1024 1024"><path d="M181.12 493.664a127.168 127.168 0 0 1 114.56-71.04h548.352v-30.592a99.2 99.2 0 0 0-99.2-99.2H601.6c-23.36 0-45.952-8.256-63.84-23.264l-111.424-93.696a99.328 99.328 0 0 0-63.84-23.232H185.952a99.2 99.2 0 0 0-99.2 99.2v432l94.336-190.176z" fill="#2595E8" ></path><path d="M879.008 487.936H295.68c-23.744 0-45.504 13.44-56.064 34.752l-143.264 288.768a99.2 99.2 0 0 0 89.6 56.8h558.816a99.328 99.328 0 0 0 90.656-58.848l0.096 0.128 100.928-234.24a62.56 62.56 0 0 0-57.44-87.36z m-176.992 309.92h-186.56a37.984 37.984 0 1 1 0-76h186.56a37.984 37.984 0 1 1 0 76z" fill="#2595E8" ></path><path d="M800.352 333.728a633.6 633.6 0 0 0-0.512-24.288 98.752 98.752 0 0 0-55.072-16.672H601.6c-23.36 0-45.952-8.256-63.84-23.264l-111.424-93.632a99.328 99.328 0 0 0-63.84-23.232H185.952a99.2 99.2 0 0 0-99.2 99.2v432l94.336-190.208a127.168 127.168 0 0 1 114.592-71.072h498.24c4.224-28.992 6.432-58.688 6.432-88.832z" fill="#3A9CED" ></path><path d="M295.68 487.936c-23.744 0-45.504 13.44-56.064 34.752l-143.264 288.768a99.2 99.2 0 0 0 89.6 56.8h296.64a609.92 609.92 0 0 0 102.624-70.4H515.52a37.984 37.984 0 1 1 0-76h144.96a606.144 606.144 0 0 0 120.224-233.92H295.68z" fill="#3A9CED" ></path><path d="M608.416 292.768H601.6c-23.36 0-45.952-8.256-63.84-23.264l-111.424-93.632a99.328 99.328 0 0 0-63.84-23.232H185.952a99.2 99.2 0 0 0-99.2 99.2v432l94.336-190.208a127.168 127.168 0 0 1 114.592-71.072h256.512a607.136 607.136 0 0 0 56.224-129.792z" fill="#59ADF8" ></path><path d="M239.616 522.688l-95.072 191.616a608.256 608.256 0 0 0 363.84-226.432H295.68a62.656 62.656 0 0 0-56.064 34.816z" fill="#59ADF8" ></path><path d="M418.976 170.304a99.2 99.2 0 0 0-56.544-17.664H185.952a99.2 99.2 0 0 0-99.2 99.2v220.128a610.144 610.144 0 0 0 332.224-301.664z" fill="#6BC2FC" ></path></symbol></svg>'), + '<svg>' + + '<symbol id="icon-play" viewBox="0 0 1024 1024"><path d="M0 0h1024v1024H0z" fill="#00BEB4" opacity=".01" ></path><path d="M161.206857 839.972571V185.929143a72.850286 72.850286 0 0 1 109.275429-63.049143l566.345143 326.948571a72.850286 72.850286 0 0 1 0 126.244572l-566.418286 326.948571a72.850286 72.850286 0 0 1-109.202286-63.049143z" fill="#00BEB4" ></path></symbol>' + + '<symbol id="icon-Pipeline" viewBox="0 0 1024 1024"><path d="M610.9184 729.6a59.392 59.392 0 0 1 59.3408 59.392v79.104a59.3408 59.3408 0 0 1-59.392 59.3408H413.1328a59.392 59.392 0 0 1-59.3408-59.392V788.992a59.392 59.392 0 0 1 59.392-59.3408h197.7856z m0-316.4672a59.392 59.392 0 0 1 59.3408 59.3408v79.104a59.3408 59.3408 0 0 1-59.392 59.392H413.1328a59.3408 59.3408 0 0 1-59.3408-59.392V472.4736a59.392 59.392 0 0 1 59.392-59.392h197.7856z m0-316.5184a59.392 59.392 0 0 1 59.3408 59.3408V235.008a59.3408 59.3408 0 0 1-59.392 59.392H413.1328A59.3408 59.3408 0 0 1 353.792 235.008V155.9552a59.392 59.392 0 0 1 59.392-59.392h197.7856z" fill="#00BEB4" ></path><path d="M749.3632 472.4224a197.8368 197.8368 0 0 1 0 395.6224l-4.608-0.256a39.5776 39.5776 0 0 1 4.608-78.848l6.9632-0.2048a118.6816 118.6816 0 0 0-6.9632-237.2096l-4.608-0.256a39.5776 39.5776 0 0 1 4.608-78.848zM274.6368 155.904l4.608 0.256a39.5776 39.5776 0 0 1-4.608 78.848 118.6816 118.6816 0 1 0 0 237.4144 39.5776 39.5776 0 1 1 0 79.104 197.7856 197.7856 0 1 1 0-395.6224z" fill="#1177D7" ></path></symbol>' + + '<symbol id="icon-dataflow-01" viewBox="0 0 1024 1024"><path d="M636.202667 214.954667c-18.688 1.493333-28.288 4.266667-34.944 7.68a85.333333 85.333333 0 0 0-37.290667 37.290666c-3.413333 6.656-6.186667 16.213333-7.68 34.944C554.666667 314.069333 554.666667 338.901333 554.666667 375.466667V469.333333h135.253333a128.042667 128.042667 0 1 1 0 85.333334H554.666667v93.866666c0 36.565333 0 61.397333 1.621333 80.597334 1.493333 18.688 4.266667 28.288 7.68 34.944a85.333333 85.333333 0 0 0 37.290667 37.290666c6.656 3.413333 16.213333 6.186667 34.944 7.68 14.08 1.152 31.232 1.493333 53.76 1.578667A128.042667 128.042667 0 0 1 938.666667 853.333333a128 128 0 0 1-248.746667 42.666667 814.037333 814.037333 0 0 1-60.672-1.877333c-23.978667-1.962667-46.037333-6.186667-66.730667-16.725334a170.666667 170.666667 0 0 1-74.581333-74.581333c-10.538667-20.693333-14.762667-42.752-16.725333-66.730667C469.333333 712.96 469.333333 684.629333 469.333333 650.325333V554.666667H334.08a128.042667 128.042667 0 1 1 0-85.333334H469.333333V373.717333c0-34.346667 0-62.72 1.877334-85.76 1.962667-24.021333 6.186667-46.08 16.725333-66.773333a170.666667 170.666667 0 0 1 74.581333-74.581333c20.693333-10.538667 42.752-14.762667 66.730667-16.725334a813.653333 813.653333 0 0 1 60.714667-1.834666 128.042667 128.042667 0 1 1 0 85.333333c-22.528 0.085333-39.68 0.426667-53.76 1.578667z" ></path></symbol>' + + '<symbol id="icon-knowledgegraph" viewBox="0 0 1024 1024"><path d="M190.464 489.472h327.68v40.96h-327.68z" ></path><path d="M482.34496 516.5056l111.26784-308.20352 38.54336 13.9264L520.86784 530.432z" ></path><path d="M620.544 196.608m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" ></path><path d="M182.272 509.952m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" ></path><path d="M558.65344 520.9088l283.77088 163.84-20.48 35.47136-283.77088-163.84z" ></path><path d="M841.728 686.08m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" ></path><path d="M448.67584 803.77856l49.60256-323.91168 40.48896 6.20544-49.60256 323.91168z" ></path><path d="M512 530.432m-143.36 0a143.36 143.36 0 1 0 286.72 0 143.36 143.36 0 1 0-286.72 0Z" ></path><path d="M462.848 843.776m-102.4 0a102.4 102.4 0 1 0 204.8 0 102.4 102.4 0 1 0-204.8 0Z"></path></symbol>' + + '<symbol id="icon-mcp" viewBox="0 0 1024 1024"><path d="M171.84 477.184l296.192-296.128q39.808-39.808 96.064-39.808 56.32 0 96.128 39.808t39.808 96.064q0 7.488-0.704 14.72 6.848-0.64 13.888-0.64 57.152 0 97.536 40.32 40.32 40.448 40.32 97.536t-40.32 97.472l-269.696 269.696q-4.928 4.928 0 9.856l57.152 57.152a30.464 30.464 0 0 1-43.072 43.072l-57.152-57.152q-19.84-19.84-19.84-48t19.84-48l269.696-269.696q22.528-22.528 22.528-54.4t-22.528-54.4q-22.592-22.592-54.4-22.592-31.936 0-54.464 22.592l-17.28 17.216L435.2 598.336a30.464 30.464 0 0 1-43.008-0.064l-0.192-0.128a30.272 30.272 0 0 1 0.128-42.88L599.68 347.52l17.408-17.408q21.952-22.016 21.952-53.056t-21.952-52.992q-22.016-22.016-53.056-22.016t-52.992 22.016L214.912 520.256a30.464 30.464 0 0 1-43.072-43.072z m567.104-29.44L513.92 672.96q-39.808 39.808-96.128 39.808t-96.128-39.808q-39.808-39.808-39.808-96.128t39.808-96.128l225.088-225.024a30.464 30.464 0 0 1 43.008 43.008L364.8 523.712q-21.952 22.016-21.952 53.056t21.952 53.056q22.016 21.952 53.056 21.952t52.992-21.952l225.088-225.088a30.464 30.464 0 0 1 43.072 43.072z" ></path></symbol>' + + '<symbol id="icon-list-end" viewBox="0 0 1024 1024"><path d="M128 554.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667zM128 298.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667zM426.666667 725.333333H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h298.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667z" fill="#757590" ></path><path d="M896 213.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667v426.666667c0 25.6-17.066667 42.666667-42.666666 42.666666h-110.933334l12.8-12.8c17.066667-17.066667 17.066667-42.666667 0-59.733333-17.066667-17.066667-42.666667-17.066667-59.733333 0l-85.333333 85.333333c-4.266667 4.266667-8.533333 8.533333-8.533334 12.8-4.266667 8.533333-4.266667 21.333333 0 34.133334 4.266667 4.266667 4.266667 8.533333 8.533334 12.8l85.333333 85.333333c8.533333 8.533333 21.333333 12.8 29.866667 12.8 8.533333 0 21.333333-4.266667 29.866666-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733333l-12.8-12.8H810.666667c72.533333 0 128-55.466667 128-128V256c0-25.6-17.066667-42.666667-42.666667-42.666667z" fill="#757590" ></path></symbol>' + + '<symbol id="icon-list-start" viewBox="0 0 1024 1024"><path d="M682.666667 469.333333H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667zM682.666667 725.333333H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h554.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667zM128 298.666667h298.666667c25.6 0 42.666667-17.066667 42.666666-42.666667s-17.066667-42.666667-42.666666-42.666667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667z" fill="#757590" ></path><path d="M810.666667 213.333333h-110.933334l12.8-12.8c17.066667-17.066667 17.066667-42.666667 0-59.733333-17.066667-17.066667-42.666667-17.066667-59.733333 0l-85.333333 85.333333c-4.266667 4.266667-8.533333 8.533333-8.533334 12.8-4.266667 8.533333-4.266667 21.333333 0 34.133334 4.266667 4.266667 4.266667 8.533333 8.533334 12.8l85.333333 85.333333c8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866666-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733333l-12.8-12.8H810.666667c25.6 0 42.666667 17.066667 42.666666 42.666666v426.666667c0 25.6 17.066667 42.666667 42.666667 42.666667s42.666667-17.066667 42.666667-42.666667V341.333333c0-72.533333-55.466667-128-128-128z" fill="#757590" ></path></symbol>' + + '<symbol id="icon-webcrawler-0" viewBox="0 0 1024 1024"><path d="M972.8 578.1504c0 10.2912-3.6352 19.0976-10.8032 26.4704-6.7328 7.1424-16.128 11.136-25.9328 11.008H807.168c0 66.8672-12.9792 124.2112-38.8352 170.496l119.4752 122.752c7.2448 7.296 10.7776 16.128 10.7776 26.4704 0 10.24-3.5328 19.072-10.7776 26.368a35.0208 35.0208 0 0 1-25.856 11.0848 34.9696 34.9696 0 0 1-25.9584-11.0592l-113.7408-116.0704c-2.176 2.176-5.0688 4.4288-8.704 7.3216-7.936 5.9392-16.0768 11.5712-24.4224 16.896-11.904 8.1152-24.448 15.232-37.504 21.3504a237.6448 237.6448 0 0 1-47.4624 16.8448 208.4352 208.4352 0 0 1-43.008 7.04 12.5696 12.5696 0 0 1-13.1584-12.8256V403.2a13.1584 13.1584 0 0 0-13.1584-13.1584H488.448a13.1584 13.1584 0 0 0-13.1584 13.1584v499.8144a12.5952 12.5952 0 0 1-13.1584 12.7744 237.9008 237.9008 0 0 1-45.1584-7.68 251.648 251.648 0 0 1-50.3552-19.1232 597.2992 597.2992 0 0 1-38.1952-22.784 189.952 189.952 0 0 1-25.1904-19.072l-8.6272-8.0896L189.44 960.256A36.864 36.864 0 0 1 162.048 972.8a35.9424 35.9424 0 0 1-35.9936-36.0192 36.48 36.48 0 0 1 8.704-27.1872l115.84-132.9664c-22.272-44.9024-33.7664-98.56-33.7664-161.024H87.936c-9.8048 0.1536-19.2-3.84-25.9328-10.9824A36.48 36.48 0 0 1 51.2 578.1504c0-10.3168 3.6352-19.072 10.8032-26.4448 6.7072-7.168 16.128-11.1872 25.9328-11.0592H216.832v-173.3888l-99.4048-101.4528a36.48 36.48 0 0 1-10.8032-26.4448c0-10.2656 3.6352-19.0976 10.8032-26.4704 6.7328-7.1168 16.128-11.0592 25.9328-10.9312 10.0864 0 18.688 3.6864 25.9328 11.008l99.4048 101.376h485.9392l99.4048-101.376a35.0464 35.0464 0 0 1 25.856-11.008c10.0864 0 18.688 3.6864 25.9328 11.008 7.2448 7.3728 10.8032 16.1792 10.8032 26.4448 0 10.2656-3.6096 19.0976-10.8032 26.4704l-98.6624 101.2992v172.7488h128.896c10.0608 0 18.7392 3.6864 25.9328 11.0592 7.2448 8.0128 10.7776 16.8448 10.7776 27.1616zM695.8848 226.176a12.5952 12.5952 0 0 1-12.7744 13.184h-342.272a12.6208 12.6208 0 0 1-12.8-13.184c2.8416-46.464 20.6848-86.2208 53.6064-119.808a175.4368 175.4368 0 0 1 130.304-55.1424c51.1744 0 94.3616 18.432 130.3552 55.0912 32.9216 32.9216 50.7648 73.2672 53.5808 119.8592z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-retrival-0" viewBox="0 0 1024 1024"><path d="M664.704 645.376a96.0256 96.0256 0 0 0 0 192 96.0256 96.0256 0 0 0 0-192zM818.9952 64H205.0048a64.0512 64.0512 0 0 0-64 64v768c0 35.3024 28.672 64 64 64h613.9904c5.8112 0 11.392-0.7936 16.7936-2.2016l-82.8928-82.8928a158.7968 158.7968 0 0 1-88.192 26.496 159.9488 159.9488 0 0 1-160-160 159.9488 159.9488 0 0 1 160-160 159.9488 159.9488 0 0 1 133.504 248.192l82.688 82.816c1.408-5.1968 2.0992-10.7008 2.0992-16.4096V128c0-35.3024-28.672-64-64-64z m-348.416 438.784c-5.7856 5.8112-13.7728 9.4208-22.5792 9.4208l-128-0.2048c-17.6128 0-32-14.4128-32-32 0-8.8064 3.584-16.7936 9.3952-22.6048 5.8112-5.7856 13.824-9.3952 22.6048-9.3952l128.1024 0.2048c17.5872 0 32 14.3872 32 32a32.4096 32.4096 0 0 1-9.4976 22.6048z m256-128.1792c-5.7856 5.7856-13.7728 9.3952-22.5792 9.3952h-384c-17.6128 0-32-14.4128-32-32 0-8.8064 3.584-16.7936 9.3952-22.6048 5.8112-5.7856 13.824-9.3952 22.6048-9.3952h384c17.6128 0 32 14.4128 32 32 0 8.8064-3.584 16.7936-9.3952 22.6048z m0-128c-5.7856 5.7856-13.7728 9.3952-22.5792 9.3952h-384c-17.6128 0-32-14.4128-32-32 0-8.8064 3.584-16.7936 9.3952-22.6048 5.8112-5.7856 13.824-9.3952 22.6048-9.3952h384c17.6128 0 32 14.4128 32 32 0 8.8064-3.584 16.7936-9.3952 22.6048z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-runcode-0" viewBox="0 0 1024 1024"><path d="M446.264832 920.6784l-97.5872-28.3136a19.2256 19.2256 0 0 1-13.1072-23.8336l218.368-752.2816a19.2256 19.2256 0 0 1 23.8592-13.1072l97.5872 28.3136c10.24 2.8928 16 13.5936 13.1072 23.8336l-218.368 752.256a19.1744 19.1744 0 0 1-23.8592 13.1328z m-182.3744-179.5072l69.6064-74.24a19.2 19.2 0 0 0-1.28-27.52l-144.9728-127.488 144.9728-127.5136a19.072 19.072 0 0 0 1.28-27.52l-69.6064-74.24a19.2 19.2 0 0 0-27.1872-0.7936L6.149632 497.8176a19.072 19.072 0 0 0 0 28.0064l230.5536 216.1408a19.072 19.072 0 0 0 27.1872-0.7936z m523.4688 0.9472l230.5536-216.1408a19.072 19.072 0 0 0 0-27.9808l-230.5536-216.32a19.3792 19.3792 0 0 0-27.1872 0.8192l-69.6064 74.24a19.2 19.2 0 0 0 1.28 27.4944l144.9728 127.6672-144.9728 127.5136a19.072 19.072 0 0 0-1.28 27.52l69.6064 74.24a19.2 19.2 0 0 0 27.1872 0.9472z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-sendemail-0" viewBox="0 0 1024 1024"><path d="M798.72 524.4928a19.328 19.328 0 0 1 20.48 4.1472l198.656 178.9952c8.192 8.32 8.192 20.8128 0 27.0848L819.2 915.7632a18.4832 18.4832 0 0 1-14.336 6.272c-2.048 0-4.096 0-6.144-2.0992-6.144-2.0992-10.24-8.32-10.24-16.64v-99.9168c-163.84 0-239.616 14.592-294.912 220.6208 0-320.512 167.936-382.976 294.912-382.976v-97.792c0-8.32 4.096-14.592 10.24-18.7648v0.0256zM829.44 0c65.536 0 118.784 54.0928 118.784 120.7552v401.664c0 12.4928-2.048 24.96-6.144 35.4048l-81.92-72.8832c-18.432-18.7648-53.248-27.0336-81.92-14.592l-145.408-151.8848 274.432-218.5728-434.176 237.2608L38.912 99.9168l276.48 218.496-227.328 237.2864 276.48-201.856 108.544 72.832 108.544-72.832 176.128 126.9248c-18.432 14.592-28.672 37.4528-28.672 62.464v41.6c-45.056 6.272-98.304 22.912-145.408 58.2912H118.784c-65.536 0-118.784-54.1184-118.784-120.7552V120.7552C0 54.1952 53.248 0 118.784 0H829.44z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-httprequest-0" viewBox="0 0 1024 1024"><path d="M805.248 51.2C916.9408 51.2 972.8 107.0592 972.8 218.752v586.496C972.8 916.9408 916.9408 972.8 805.248 972.8H218.752C107.0592 972.8 51.2 916.9408 51.2 805.248V218.752C51.2 107.0592 107.0592 51.2 218.752 51.2h586.496z m-18.0224 609.1776a20.9408 20.9408 0 0 0-19.712 7.4496 325.1968 325.1968 0 0 1-166.016 106.8544c16.0768-22.656 29.7984-51.4048 40.8576-84.1728a20.9408 20.9408 0 0 0-39.68-13.4656c-12.2112 36.1984-27.0336 64.3072-42.8032 82.944-15.7952 18.7648-30.5664 25.9072-43.3408 25.9072-12.8 0-27.5712-7.168-43.3664-25.9072-13.7728-16.2816-26.8544-39.8848-38.0672-69.7344l-4.7104-13.1584-1.7408-3.8912a20.9408 20.9408 0 0 0-38.8864 13.184l0.9216 4.096 5.12 14.3872c10.112 26.8544 22.0928 50.56 35.7632 69.7856a325.0432 325.0432 0 0 1-166.0416-106.8288l-1.9712-2.048a20.9408 20.9408 0 0 0-30.2848 28.7744l6.4256 7.5776a367.104 367.104 0 0 0 276.8384 125.6448 367.104 367.104 0 0 0 283.264-133.2224 20.9408 20.9408 0 0 0-12.544-34.176zM210.048 411.8784H159.744v201.3696h50.304V529.92h67.6864v83.3536h50.3296V411.904h-50.3296v67.6608H210.048v-67.6608z m638.7456-0.0512l-117.9648 0.0512v201.3184h50.3552v-67.6352h67.6608a48.1792 48.1792 0 0 0 35.4048-14.9504c9.9328-9.984 14.9504-21.7856 14.9504-35.4048v-33.024c0-13.6448-4.992-25.472-14.9504-35.4304a48.5632 48.5632 0 0 0-35.456-14.9248z m-151.0144 0h-151.0144v50.3552h50.3552v150.9888h50.3552v-151.04h50.304v-50.304z m-185.6512 0h-151.04v50.3552h50.3808v150.9888h50.304v-151.04h50.3552v-50.304z m336.6656 50.3296v33.0752h-67.584v-33.0752h67.584zM516.5056 196.224A367.104 367.104 0 0 0 233.2928 329.472l-1.664 2.2528a20.9408 20.9408 0 0 0 33.92 24.4736l6.0672-7.168a325.1968 325.1968 0 0 1 160.0512-99.712c-13.7216 19.2512-25.728 43.008-35.84 69.888l-5.12 14.3616a20.9664 20.9664 0 0 0 39.7056 13.4144l4.6592-13.1584c11.3152-29.8752 24.32-53.4016 38.144-69.76 15.7696-18.7648 30.5664-25.856 43.3408-25.856 12.7744 0 27.5456 7.1424 43.3408 25.856 13.7984 16.3072 26.88 39.8848 38.144 69.76l4.6848 13.1584 1.7408 3.8912a20.9408 20.9408 0 0 0 38.8864-13.2096l-0.9728-4.096-5.12-14.3616c-10.112-26.88-22.0928-50.6368-35.84-69.888a325.0432 325.0432 0 0 1 154.5728 93.6448l11.5712 13.2352a20.9408 20.9408 0 0 0 34.5856-23.2192l-2.3296-3.5072a367.104 367.104 0 0 0-283.264-133.2224z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-executesql-0" viewBox="0 0 1024 1024"><path d="M64 649.2928c96 77.6192 269.6448 118.272 443.136 118.272 173.568 0 347.2128-40.6528 443.2128-118.2208v136.6528h-1.92c-22.1696 94.208-210.432 166.1952-441.2928 166.1952-230.784 0-419.072-72.0128-441.2672-166.1952H64v-136.704z m129.28 118.272a37.0688 37.0688 0 0 0-36.992 36.864 36.992 36.992 0 0 0 73.9072 0 37.0688 37.0688 0 0 0-36.992-36.864h0.0768z m757.0688-376.7808v118.1952c0 101.5808-197.632 184.6528-443.2128 184.6528-245.504 0-443.136-82.9952-443.136-184.6272v-118.2208c96 77.696 269.6448 118.1952 443.136 118.1952 173.568 0 347.2128-40.6272 443.2128-118.1952zM193.2288 508.9792a37.0432 37.0432 0 0 0-36.9408 36.9408 36.992 36.992 0 0 0 73.9072 0 37.0432 37.0432 0 0 0-36.992-36.9152zM551.4752 64c43.9552 1.92 87.808 6.784 131.072 14.72 16.64 3.7632 33.28 7.424 48 11.136 22.5792 5.4528 44.8 12.16 66.56 20.352 20.2752 9.1392 40.4992 16.5632 57.216 25.8048 7.3472 5.5552 16.5632 9.216 23.9104 14.72 3.712 1.92 7.5008 5.5552 11.136 7.424l5.4528 3.6864c3.584 2.5088 6.8352 4.9408 9.2928 7.3728 4.1472 2.56 7.8592 5.632 11.0592 9.2672l16.64 20.3008c12.928 16.64 18.5088 33.28 18.5088 51.712 0 101.5552-197.632 184.6528-443.2128 184.6528C261.632 435.2 64 352.128 64 250.496c0-15.6928 4.4032-31.0528 12.8512-44.288 6.2976-10.112 13.7216-19.456 22.2208-27.776 12.8512-12.8 27.6992-23.936 46.208-34.9952l18.432-11.0848c16.64-7.424 33.28-14.72 53.5808-22.144 20.2752-7.424 42.4192-12.928 64.6144-18.56 14.72-3.584 31.3856-5.504 48-9.1392a943.5648 943.5648 0 0 1 131.072-14.7968c16.64-1.92 31.3856-1.92 46.1568-1.92 14.72 0 29.6448 0 44.3648-1.792z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-less-or-equal" viewBox="0 0 1024 1024"><path d="M320.731429 441.636571L730.697143 266.605714 730.185143 219.428571l-520.045714 222.134858 520.045714 228.205714 0.438857-52.443429L320.731429 441.782857zM730.697143 804.571429l0.804571-50.176-518.802285-232.009143-2.486858 50.029714L730.550857 804.571429z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-not-contains" viewBox="0 0 1024 1024"><path d="M579.291429 146.285714c18.432 19.017143 25.380571 34.596571 20.992 46.738286l-33.206858 90.624h22.162286c114.688 0 208.457143 84.845714 214.966857 197.485714L804.571429 493.714286c0 118.857143-96.402286 209.993143-215.332572 209.993143H413.622857l-51.053714 139.922285a35.035429 35.035429 0 1 1-65.755429-24.137143l41.691429-115.785142H219.428571v-59.977143L360.155429 643.657143l108.251428-300.032H219.428571v-59.977143l270.628572-0.073143 42.057143-116.443428c4.388571-12.068571 20.187429-19.017143 47.177143-20.918858zM435.565714 643.730286h153.673143c89.234286 0 161.499429-60.708571 161.499429-149.942857 0-89.234286-72.265143-150.089143-161.499429-150.089143h-44.105143L435.565714 643.730286z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-Less" viewBox="0 0 1024 1024"><path d="M339.529143 511.122286L804.571429 749.202286l-0.073143 55.369143-569.563429-293.595429L804.571429 219.428571v57.124572z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-Greater-or-equal" viewBox="0 0 1024 1024"><path d="M630.125714 441.636571L220.16 266.605714 220.672 219.428571l520.045714 222.134858-520.045714 228.205714-0.438857-52.443429L630.125714 441.782857zM220.16 804.571429L219.428571 754.395429l518.802286-232.009143 2.486857 50.029714L220.306286 804.571429z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-equal" viewBox="0 0 1024 1024"><path d="M219.428571 545.792h585.142858v44.982857H219.428571v-44.982857zM219.428571 365.714286h585.142858v44.982857H219.428571V365.714286z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-not-equals" viewBox="0 0 1024 1024"><path d="M462.116571 578.56l86.308572-134.875429H219.428571v-44.909714h357.814858L692.077714 219.428571h51.2l-112.64 179.346286h172.836572v44.909714H601.819429L515.510857 578.56h287.963429v44.909714H486.692571L370.614857 804.571429h-51.638857l114.322286-181.174858H219.428571V578.56z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-Contains" viewBox="0 0 1024 1024"><path d="M219.428571 352.548571h369.810286c89.234286 0 161.499429 60.854857 161.499429 150.089143 0 89.234286-72.338286 150.016-161.499429 150.016H219.428571v59.977143h369.810286C708.169143 712.704 804.571429 621.641143 804.571429 502.710857S708.169143 292.571429 589.238857 292.571429H219.428571v59.977142z" fill="#939393" ></path></symbol>' + + '<symbol id="icon-KR" viewBox="0 0 1024 1024"><path d="M440.4736 204.8a76.8 76.8 0 0 1 60.7744 29.7984l36.352 47.0016 307.6096 1.4336A76.8 76.8 0 0 1 921.6 359.8336V742.4a76.8 76.8 0 0 1-76.8 76.8h-614.4A76.8 76.8 0 0 1 153.6 742.4v-460.8A76.8 76.8 0 0 1 230.4 204.8h210.0736zM537.6 416a115.2 115.2 0 1 0 58.4704 214.4768l36.352 36.3008a28.7744 28.7744 0 0 0 40.7552-40.7552l-36.352-36.352A115.2 115.2 0 0 0 537.6 416z m0 57.6a57.6 57.6 0 1 1 0 115.2 57.6 57.6 0 0 1 0-115.2zM440.4736 262.4H230.4a19.2 19.2 0 0 0-18.8928 15.7696l-0.3072 3.84h253.8496L455.68 269.824a19.2 19.2 0 0 0-15.1552-7.4752z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-KR1" viewBox="0 0 1024 1024"><path d="M440.4736 204.8a76.8 76.8 0 0 1 60.7744 29.7984l36.352 47.0016 307.6096 1.4336A76.8 76.8 0 0 1 921.6 359.8336V742.4a76.8 76.8 0 0 1-76.8 76.8h-614.4A76.8 76.8 0 0 1 153.6 742.4v-460.8A76.8 76.8 0 0 1 230.4 204.8h210.0736zM537.6 416a115.2 115.2 0 1 0 58.4704 214.4768l36.352 36.3008a28.7744 28.7744 0 0 0 40.7552-40.7552l-36.352-36.352A115.2 115.2 0 0 0 537.6 416z m0 57.6a57.6 57.6 0 1 1 0 115.2 57.6 57.6 0 0 1 0-115.2zM440.4736 262.4H230.4a19.2 19.2 0 0 0-18.8928 15.7696l-0.3072 3.84h253.8496L455.68 269.824a19.2 19.2 0 0 0-15.1552-7.4752z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-code-set" viewBox="0 0 1024 1024"><path d="M808.0384 80.0768c20.48 18.5856 37.2224 35.328 50.2784 46.5408 13.0048 13.0048 22.3232 24.1664 27.904 33.4848 7.4752 9.3184 11.1616 16.7936 13.056 22.3744 1.8432 5.5808 1.8432 11.1616 1.8432 16.7424v18.6368h-171.264c-9.3184 0-14.8992-3.7376-22.3744-9.3184-7.424-5.632-11.1616-13.056-14.848-20.48-3.7376-7.4752-7.4752-14.8992-9.3696-24.2176-1.8432-9.3184-3.6864-14.848-3.6864-20.48V0h1.8432c7.4752 0 13.056 0 20.48 1.8432 5.632 1.8944 13.056 5.632 22.3232 11.2128 11.2128 5.5808 22.3744 14.848 35.3792 24.1664 13.056 11.1616 27.9552 24.2176 48.4352 42.8544zM608.768 145.2032c0 14.8992 1.8432 31.6928 7.424 48.4352 5.632 16.7424 11.2128 33.4848 22.3744 46.5408 11.1616 13.0048 22.3232 26.0608 39.0656 35.328 14.8992 9.3696 33.536 14.9504 54.016 14.9504h171.264V875.008c0 22.3232-3.6864 42.8032-13.0048 61.44s-20.48 33.4848-35.3792 46.5408c-14.8992 13.056-31.6416 22.3232-48.4352 29.7984-16.7424 7.424-35.328 11.1616-52.1216 11.1616H251.392c-14.848 0-31.6416-3.7376-48.384-13.056a281.088 281.088 0 0 1-48.4352-33.4848c-14.848-14.8992-27.904-29.7984-37.2224-48.4352-9.3184-18.5856-14.8992-37.2224-14.8992-55.808V150.784c0-16.7424 3.7376-33.4848 11.1616-52.1216 7.4752-18.5856 18.6368-33.4848 31.6416-48.384 13.056-14.8992 27.9552-26.112 44.6976-35.3792C206.6432 5.5808 223.4368 0 242.0224 0h374.272c-7.4752 0-7.4752 145.2032-7.4752 145.2032z m-94.976 526.8992c9.3184-9.3184 14.8992-22.3232 14.8992-37.2224 0-14.848-3.6864-27.904-13.0048-35.328l-189.952-193.6896a47.6672 47.6672 0 0 0-35.328-14.848c-13.056 0-24.2176 5.5296-35.3792 14.848a50.8416 50.8416 0 0 0-14.8992 35.3792c0 13.056 5.5808 24.2176 14.848 35.3792L411.4944 634.88l-154.5216 158.208a47.6672 47.6672 0 0 0-14.848 35.4304c0 14.848 5.5296 26.0608 14.848 35.328 9.3184 9.3696 20.48 14.9504 35.3792 14.9504 13.056 0 24.2176-5.632 35.3792-14.8992l186.1632-191.7952z m240.1792 130.3552h-219.648v74.4448h219.648v-74.4448z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-a-ContentRewrite" viewBox="0 0 1024 1024"><path d="M842.9568 499.8144l82.176 82.176a24.1664 24.1664 0 0 1 0 34.1504l-301.568 301.568a23.8592 23.8592 0 0 1-11.4176 6.3488l-115.2 27.7504a24.1664 24.1664 0 0 1-28.8256-30.3616L491.52 842.752v0.1024l9.1648-30.7712a24.6784 24.6784 0 0 1 1.4848-3.7376l-1.4848 3.6864a23.6544 23.6544 0 0 1 6.0928-10.1888l302.0288-302.0288a24.1664 24.1664 0 0 1 34.1504 0z m-177.6128-418.2016v0.0512l51.968 0.0512c55.1936 0 99.9424 44.7488 99.9424 99.9424h0.0512V415.744l-361.0112 348.928-31.0272 106.7008h-0.1024l-9.3696 32.3584H216.8832a99.9424 99.9424 0 0 1-99.9424-99.9424V181.6576c0-22.5792 7.4752-43.4176 20.1216-60.16 18.2784-24.2688 47.2576-39.8848 79.872-39.8848z m-222.6688 478.5664H298.752a49.664 49.664 0 0 0-0.0512 99.328h143.9744a49.664 49.664 0 1 0 0-99.328z m192.8192-164.7616H298.7008a49.664 49.664 0 0 0-44.5952 27.8016l-2.1504 5.0688a49.664 49.664 0 0 0 46.7456 66.4064h193.1264l-0.1024 0.1024 143.8208-0.0512a49.664 49.664 0 1 0-0.0512-99.328zM298.7008 242.432a49.5104 49.5104 0 0 0-48.9984 57.7536l-0.3072-2.2016a49.152 49.152 0 0 0 1.024 5.8368l-0.7168-3.6352a49.408 49.408 0 0 0 1.28 5.6832l-0.512-2.048c0.4608 1.8944 1.024 3.7888 1.6896 5.5808l-1.1776-3.5328c0.6144 2.048 1.3312 4.096 2.2016 5.9904l-1.024-2.4576c0.5632 1.536 1.2288 3.072 1.9456 4.5056l-0.9216-2.048c0.7168 1.6896 1.536 3.328 2.4576 4.9152l-1.536-2.8672a49.664 49.664 0 0 0 3.3792 5.8368l-1.8432-2.9696a49.664 49.664 0 0 0 3.2768 4.9152l-1.4336-1.9456c0.8704 1.28 1.792 2.56 2.816 3.7376l-1.3824-1.792c1.1776 1.5872 2.4576 3.072 3.7888 4.5568l-2.4064-2.7648c1.28 1.5872 2.7136 3.072 4.1984 4.5568l-1.792-1.792a49.5104 49.5104 0 0 0 35.9936 15.4112h301.1584v0.0512h35.6864a49.664 49.664 0 1 0-0.0512-99.328z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-await" viewBox="0 0 1024 1024"><path d="M814.1824 138.7008a100.4032 100.4032 0 0 1 34.2528 6.144l1.1264 0.4096a98.0992 98.0992 0 0 1 21.8624 11.776l1.8432 1.3824c1.6896 1.2288 3.2768 2.5088 4.864 3.84l1.3824 1.2288a85.0944 85.0944 0 0 1 11.3664 11.8272l-5.2736-5.9904c17.0496 17.7664 27.4432 41.8816 27.4432 68.4544v476.8256a99.072 99.072 0 0 1-99.072 99.072h-168.0896a32.4608 32.4608 0 0 0-22.9888 9.6256l-79.616 80.5376a32.4096 32.4096 0 0 1-47.0016-1.024l-71.2192-78.5408a32.3584 32.3584 0 0 0-23.9616-10.5984H210.8416a99.1232 99.1232 0 0 1-99.1232-99.1232V237.824c0-54.7328 44.3904-99.1232 99.1232-99.1232zM291.9936 427.8784l-6.4 0.3584a58.88 58.88 0 1 0 6.4-0.3584z m224.2048 0l-6.3488 0.3584a58.88 58.88 0 0 0-52.224 65.3312l-0.2048-3.072c0.1536 2.0992 0.4096 4.1984 0.7168 6.2976l-0.512-3.2256c0.3072 2.304 0.7168 4.5568 1.2288 6.7584l-0.7168-3.584c0.4096 2.4064 0.9728 4.6592 1.6384 6.912l-0.9216-3.328c0.512 2.2016 1.1776 4.352 1.9456 6.5024l-1.024-3.1744a58.9312 58.9312 0 1 0 91.2896-64.4096l-1.1264-0.7168a59.0336 59.0336 0 0 0-12.6976-6.8096l0.6144 0.3072a58.368 58.368 0 0 0-6.4-2.1504l5.7856 1.8432a58.4704 58.4704 0 0 0-6.8608-2.1504l1.0752 0.3072a58.7264 58.7264 0 0 0-15.2576-1.9968z m224.3072 0l-6.4 0.3584a58.88 58.88 0 1 0 6.4-0.3584z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-a-QuestionClassification" viewBox="0 0 1024 1024"><path d="M607.0272 127.1808a51.0976 51.0976 0 0 0-87.6544 0l-153.6 256A51.2 51.2 0 0 0 409.6 460.6464h307.2a51.0464 51.0464 0 0 0 43.8272-77.4656l-153.6-256zM563.2 601.6v230.4c0 35.328 28.672 64 64 64h230.4c35.328 0 64-28.672 64-64v-230.4c0-35.328-28.672-64-64-64h-230.4c-35.328 0-64 28.672-64 64zM307.2 921.6a204.8 204.8 0 1 0 0-409.6 204.8 204.8 0 0 0 0 409.6z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-condition" viewBox="0 0 1024 1024"><path d="M927.4368 267.8784L801.7408 144.384h-259.584V64H427.8784v80.384H219.2384v241.3056H801.792l125.6448-117.76zM427.8784 960h114.2784v-241.3056H427.8784v241.3056z m-331.264-404.9408l125.6448 117.76H804.864V431.616H222.2592l-125.696 123.4432zM763.904 472.9856v158.5152H235.9296L151.04 555.008l84.8384-82.0736h528.0768z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-agent-ai" viewBox="0 0 1024 1024"><path d="M121.2416 347.9552l343.296 199.3728v278.1696h-0.0512v119.1424a110.592 110.592 0 0 1-5.4272-2.9184l-287.0784-166.7072a103.7824 103.7824 0 0 1-51.712-90.0608l0.8192-331.9808c0-1.6896 0.0512-3.328 0.1536-5.0176z m782.3872 2.0992c0.1024 1.5872 0.1536 3.2256 0.1536 4.864l-0.256 91.9552-0.6144 240.0256a103.7824 103.7824 0 0 1-52.1216 89.8048l-287.9488 165.2736-3.2768 1.7408h-0.0512v-395.4176l344.064-198.2464v0.0512zM461.1584 97.8944a103.7824 103.7824 0 0 1 103.8336 0.256l189.5424 110.08 97.5872 56.6784 4.1984 2.6624-343.4496 197.888h-0.0512l-343.808-199.68c1.3824-0.9216 2.7648-1.792 4.1984-2.6112z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-reply" viewBox="0 0 1024 1024"><path d="M666.7776 169.5744c26.4192 0 50.5344 9.5232 69.2736 25.2928a99.4816 99.4816 0 0 1 10.24 9.8304l-4.5056-4.608 2.7648 2.7648 1.7408 1.8432v0.0512c9.984 10.8544 17.8176 23.8592 22.6816 38.1952h21.76a144.5888 144.5888 0 0 1 54.8864 10.9568l2.2016 0.9216a142.1312 142.1312 0 0 1 46.1312 32.256c8.1408 8.4992 15.2064 17.92 21.0432 28.16l1.6896 3.072a112.2304 112.2304 0 0 1 7.1168 15.36l-3.2256-7.5776c0.7168 1.4848 1.3824 2.9696 1.9968 4.5056l1.2288 3.072v0.1024c6.4 16.1792 9.8816 33.792 9.8816 52.1728v290.6624a135.0656 135.0656 0 0 1-134.912 134.8608 5.632 5.632 0 0 0-5.5808 5.5808v23.808c0 32.1536-18.2784 60.2112-47.7696 73.1136a79.5136 79.5136 0 0 1-86.1696-14.3872l-94.2592-86.6816a5.5296 5.5296 0 0 0-3.7888-1.4848H450.7136a42.496 42.496 0 0 1-6.5536-0.512l-1.536-0.3072a32.3072 32.3072 0 0 1-3.7888-0.8704l-0.5632-0.2048a34.4064 34.4064 0 0 1-4.608-1.6896l-0.8704-0.4608a30.1568 30.1568 0 0 1-3.4304-1.7408l-1.3312-0.8704a29.696 29.696 0 0 1-3.072-2.048l-1.1264-1.024a31.9488 31.9488 0 0 1-2.9184-2.56l-0.8192-0.8704a34.304 34.304 0 0 1-2.7648-3.1744l-0.6144-0.8192a34.304 34.304 0 0 1-2.048-2.9696l-1.024-1.792a41.0624 41.0624 0 0 1-5.2736-20.4288c0-23.3472 18.9952-42.3424 42.3424-42.3424h110.5408c22.6816 0 44.4416 8.4992 61.1328 23.8592 0.3072 0.256 0.256 0.256 0.256 0.3072l85.8112 78.8992v-12.7488c0-49.8176 40.4992-90.3168 90.3168-90.3168 27.648 0 50.176-22.528 50.176-50.176V385.8944a58.368 58.368 0 0 0-58.2656-58.2656h-15.872v253.5424a108.032 108.032 0 0 1-108.032 108.032H443.7504c-12.9024 0-25.344 4.864-34.8672 13.6192l-101.2224 93.0816a39.68 39.68 0 0 1-22.6304 10.496l-4.7616 0.2048a40.2944 40.2944 0 0 1-40.1408-40.3456v-25.5488c0-28.4672-23.0912-51.5072-51.5072-51.5072a99.328 99.328 0 0 1-99.328-99.328V277.76a108.032 108.032 0 0 1 108.032-108.1856z m-153.7024 303.9744h-239.104a39.5776 39.5776 0 1 0 0 79.2576h173.312l-0.1024 0.0512 65.8944-0.0512a39.6288 39.6288 0 0 0 30.1568-65.3312l2.4576 3.2256a39.7824 39.7824 0 0 0-3.3792-4.2496l0.9216 1.024a40.1408 40.1408 0 0 0-3.5328-3.584l2.6112 2.56a39.8848 39.8848 0 0 0-4.1984-3.9936l1.5872 1.3824a40.0384 40.0384 0 0 0-4.0448-3.2256l2.4576 1.8432a39.7312 39.7312 0 0 0-4.1984-2.9696l1.7408 1.1264a39.7824 39.7824 0 0 0-4.7616-2.816l3.072 1.6896a39.5264 39.5264 0 0 0-4.7104-2.5088l1.6384 0.8192a39.424 39.424 0 0 0-5.632-2.304l3.9936 1.4848a39.3728 39.3728 0 0 0-5.0176-1.8432l1.024 0.3584a39.2704 39.2704 0 0 0-4.7104-1.2288l3.6864 0.8704a39.424 39.424 0 0 0-5.6832-1.2288l-5.4784-0.3584zM237.4656 374.0672a39.6288 39.6288 0 0 0 36.5056 24.2176h319.2832a39.7312 39.7312 0 0 0 11.008-1.536l-1.6896 0.4096a39.3216 39.3216 0 0 0 4.7616-1.4336l-3.072 1.024a39.6288 39.6288 0 0 0 5.632-2.048l-2.56 1.024a39.424 39.424 0 0 0 5.12-2.3552l-2.56 1.3312a39.6288 39.6288 0 0 0 4.7104-2.6112l-2.2016 1.28a39.8336 39.8336 0 0 0 15.872-16.0768l-1.8432 3.0208a39.5776 39.5776 0 0 0 2.6112-4.608l-0.768 1.536a39.6288 39.6288 0 0 0 2.1504-4.864l-1.3824 3.2768a39.3728 39.3728 0 0 0 2.2016-5.632l-0.8192 2.3552a39.6288 39.6288 0 0 0 1.5872-5.4784l-0.768 3.1232a39.424 39.424 0 0 0 1.2288-5.7344l0.1536-2.4576 0.256-3.1744a40.192 40.192 0 0 0-0.4096-5.7856l0.3072 2.816a39.6288 39.6288 0 0 0-0.6144-4.608l0.3072 1.792a39.6288 39.6288 0 0 0-1.3824-5.9904l1.024 4.1984a39.3216 39.3216 0 0 0-1.4848-5.632l0.4608 1.4336a39.6288 39.6288 0 0 0-2.304-5.6832l1.8432 4.3008a39.424 39.424 0 0 0-2.2528-5.1712l0.4096 0.8704a39.6288 39.6288 0 0 0-2.9696-5.12l2.56 4.2496a39.6288 39.6288 0 0 0-3.2768-5.2224l-3.84-4.4032a39.6288 39.6288 0 0 0-5.4784-4.6592l0.9728 0.7168a39.6288 39.6288 0 0 0-5.376-3.2768l4.4032 2.56a39.6288 39.6288 0 0 0-9.7792-4.864l5.12 2.0992a39.3728 39.3728 0 0 0-5.888-2.4064l0.768 0.3072a39.6288 39.6288 0 0 0-6.0416-1.536l5.2736 1.2288a39.3728 39.3728 0 0 0-5.888-1.3824l-6.0928-0.4096H273.9712a39.6288 39.6288 0 0 0-7.8848 0.768l2.1504-0.3584a39.6288 39.6288 0 0 0-5.7344 1.28l3.584-0.9216a39.3216 39.3216 0 0 0-5.5296 1.536l1.9456-0.6144a39.6288 39.6288 0 0 0-5.7856 2.2528l3.84-1.5872a39.424 39.424 0 0 0-5.2224 2.304l1.3824-0.7168a39.6288 39.6288 0 0 0-4.8128 2.7648l3.4304-2.048a39.68 39.68 0 0 0-5.1712 3.2768l-4.1984 3.6864a39.6288 39.6288 0 0 0-4.864 5.888l1.1776-1.6896a39.6288 39.6288 0 0 0-3.2768 5.1712l2.048-3.4816a39.5776 39.5776 0 0 0-2.7648 4.864l0.7168-1.3824a39.6288 39.6288 0 0 0-2.304 5.2224l1.5872-3.84a39.3728 39.3728 0 0 0-2.2528 5.7856l0.6656-1.9456a39.6288 39.6288 0 0 0-1.536 5.5296l0.8704-3.584a39.424 39.424 0 0 0-1.28 5.7856l-0.2048 2.3552-0.2048 3.328a39.168 39.168 0 0 0 0.3584 5.3248l1.024 4.864 1.024 3.4816 0.6144 1.4336z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-a-textprocessing" viewBox="0 0 1024 1024"><path d="M751.616 102.4a102.4 102.4 0 0 1 102.4 102.4v640a102.4 102.4 0 0 1-102.4 102.4H256a102.4 102.4 0 0 1-102.4-102.4V204.8a102.4 102.4 0 0 1 102.4-102.4h495.616zM370.688 307.2h-43.4176l-10.5472 16.1792a405.0944 405.0944 0 0 0-39.7312 84.992A396.9536 396.9536 0 0 0 256 538.112c0 44.3904 6.8608 87.808 20.992 129.2288 11.8784 34.56 28.3136 68.096 50.2784 100.6592h43.3664l-9.216-17.2032a499.8656 499.8656 0 0 1-34.6112-84.9408 419.8912 419.8912 0 0 1 0-256.512c10.0352-34.048 24.6272-68.096 43.8272-102.144z m326.0416 0h-43.3664l10.752 20.4288c13.6704 27.2384 24.6784 54.4768 33.0752 81.7152 13.2608 42.3936 20.1216 85.3504 20.1216 128.256 0 42.4448-6.8608 85.3504-20.1216 128.256-10.496 33.5872-25.088 67.584-43.8272 102.144h43.3664l10.3936-16.384a436.736 436.736 0 0 0 39.8848-84.2752 409.4464 409.4464 0 0 0 0-259.0208A421.6832 421.6832 0 0 0 696.7296 307.2zM462.8992 460.1344H402.176l81.2544 119.3984-91.8016 135.68h61.2352l59.392-94.208 58.88 94.208h61.2352l-91.8016-135.68 81.2544-119.3984H561.152l-48.896 77.9776-49.3056-77.9776z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-loop" viewBox="0 0 1024 1024"><path d="M510.9248 77.7216a440.0128 440.0128 0 0 1 17.1008 0.3584l8.6016 0.4096a436.3776 436.3776 0 0 1 4.6592 0.3072l2.304 0.1536a434.5856 434.5856 0 0 1 27.5968 2.9696l3.4816 0.512 2.7136 0.4096 4.4544 0.7168 1.8432 0.256a430.2336 430.2336 0 0 1 10.4448 1.9456l1.8944 0.4096 3.3792 0.6656 1.024 0.2048a429.1584 429.1584 0 0 1 11.2128 2.5088l1.9456 0.512 3.1232 0.7168a426.752 426.752 0 0 1 24.7808 7.0656l2.4576 0.768 11.008 3.7376 2.4064 0.8704c13.1584 4.7616 26.0608 10.0864 38.6048 16.0768l2.3552 1.1264-2.3552-1.1264A432.2304 432.2304 0 1 1 78.5408 509.952l0.3584 18.0736a432.5888 432.5888 0 0 1 15.6672-135.168c10.8032-38.4 26.7776-74.752 47.104-108.032l6.5024-10.24a434.5856 434.5856 0 0 1 89.088-99.328l3.584-2.8672a304.64 304.64 0 0 1 5.9904-4.7616l3.072-2.304c8.8064-6.7072 17.92-13.1072 27.3408-19.1488l2.7136-1.7408a416 416 0 0 1 63.744-33.28 774.6048 774.6048 0 0 1 12.6976-5.0688l1.5872-0.5632c3.3792-1.28 6.7584-2.56 10.1376-3.6864l2.4064-0.8704a404.5824 404.5824 0 0 1 27.3408-8.3456l6.9632-1.8432a429.056 429.056 0 0 1 2.56-0.6144l2.5088-0.6144a429.2096 429.2096 0 0 1 100.8128-11.8272z m71.8848 403.6096a15.2064 15.2064 0 0 0-3.2768 0.4096l-2.6624 0.9216a14.848 14.848 0 0 0-9.0624 13.8752v83.8656H382.3616c-56.2176 0-84.8896-20.5824-109.056-47.8208-4.352-4.864-12.4416-1.1264-11.52 5.3248 8.3456 58.88 35.6864 91.904 67.1744 110.3872h-0.0512c36.5056 21.504 78.592 23.3472 103.3216 23.3472h135.5264v0.0512h0.0512v85.1968c0 8.9088 7.2704 15.0528 15.0016 15.2064l3.2768-0.3072a15.1552 15.1552 0 0 0 6.4-3.072l161.6896-130.1504a15.2064 15.2064 0 0 0 0-23.7056l-161.6896-130.1504a15.104 15.104 0 0 0-6.4-3.072zM438.784 235.6224l-3.328 0.3072a15.1552 15.1552 0 0 0-6.4 3.072L267.3664 369.152a15.1552 15.1552 0 0 0-2.4064 2.4064l-1.024 1.3824a15.2064 15.2064 0 0 0 3.4304 19.8656l11.9296 9.5744 149.8112 120.5248a15.1552 15.1552 0 0 0 6.4 3.072l3.2768 0.3584 3.2768-0.4608c6.4-1.536 11.7248-7.168 11.7248-14.7968V427.3152h185.4464l10.9056 0.256c49.152 2.56 75.5712 22.1184 98.1504 47.5648a6.4512 6.4512 0 0 0 5.7856 2.1504l1.9456-0.5632a6.4512 6.4512 0 0 0 3.7888-6.912c-10.752-75.4688-52.4288-108.4928-94.208-122.88a199.5776 199.5776 0 0 0-42.496-9.216l1.1776 0.1536a252.2112 252.2112 0 0 0-9.8304-0.9728l8.704 0.8192a257.024 257.024 0 0 0-10.8544-0.9728l2.1504 0.1536a278.8864 278.8864 0 0 0-7.8336-0.512l5.6832 0.3584a288.0512 288.0512 0 0 0-8.0896-0.4096l2.4064 0.1024a315.0848 315.0848 0 0 0-8.4992-0.3072l6.144 0.2048a331.1616 331.1616 0 0 0-8.3968-0.256l-6.5536-0.0512H453.7856V250.8288a15.104 15.104 0 0 0-11.776-14.7968l-3.2256-0.4096z" fill="currentColor" ></path></symbol>' + + '<symbol id="icon-file-sub" viewBox="0 0 1024 1024"><path d="M592 93.184c-44.16-0.96-98.496-22.528-120.064-48.64-21.824-26.24-74.176-46.72-116.992-44.352-79.04 3.968-157.888 10.88-236.416 20.672C75.776 26.432 35.072 68.032 29.376 112.064a3154.048 3154.048 0 0 0 0 782.592c5.76 43.968 46.4 85.568 89.152 91.136 262.72 32.896 525.44 32.896 788.224 0 42.688-5.568 83.392-47.168 89.088-91.136 28.8-231.04 32.064-462.08 9.856-693.12-4.352-44.032-44.992-84.608-88.832-89.152a4115.072 4115.072 0 0 0-324.928-19.2z" fill="#DEA151" ></path><path d="M995.84 894.592c-5.696 44.032-46.4 85.632-89.088 91.2-262.784 32.896-525.44 32.896-788.224 0-42.752-5.568-83.456-47.168-89.152-91.2A3156.928 3156.928 0 0 1 6.656 402.56c1.472-44.032 41.088-82.304 86.656-84.544 279.424-12.608 559.232-12.608 838.592 0 45.632 2.24 85.248 40.512 86.72 84.544a3155.968 3155.968 0 0 1-22.784 492.032z" fill="#F0CA58" ></path><path d="M143.36 190.528a5356.288 5356.288 0 0 1 745.152 0.448c3.008 41.792 5.376 83.584 7.168 125.44A9285.44 9285.44 0 0 0 136.256 316.16c1.792-41.92 4.096-83.84 7.04-125.632z" fill="#F2F2F2" ></path></symbol>' + + '<symbol id="icon-reset" viewBox="0 0 1024 1024"><path d="M672.694857 864.694857H256.731429a32.036571 32.036571 0 0 1 0-64h415.963428a192 192 0 0 0 0-384H202.166857l139.629714 139.483429a30.646857 30.646857 0 1 1-43.446857 43.300571L104.009143 405.430857a30.646857 30.646857 0 0 1 0-43.373714l194.340571-194.048a30.646857 30.646857 0 1 1 43.446857 43.373714L200.265143 352.621714h472.502857a256 256 0 0 1 0 512z" fill="#3BA05C" ></path></symbol>' + + '<symbol id="icon-speak" viewBox="0 0 1024 1024"><path d="M512 608c88 0 160-72 160-160V256c0-88-72-160-160-160A160.448 160.448 0 0 0 352 256v192c0 88 72 160 160 160z m284.608-115.584a31.936 31.936 0 1 0-63.232-9.792A225.472 225.472 0 0 1 512 672a225.536 225.536 0 0 1-221.44-189.44 32 32 0 1 0-63.168 9.728A285.952 285.952 0 0 0 480 734.08V832H384a32 32 0 1 0 0 64h256a32 32 0 1 0 0-64H544v-97.92a285.952 285.952 0 0 0 252.608-241.664z" fill="#979AAB" ></path></symbol>' + + '<symbol id="icon-a-preview-file" viewBox="0 0 1024 1024"><path d="M648.305778 170.666667c58.026667 0 105.244444 46.876444 105.244444 104.561777v258.161778h-52.053333V274.659556a52.906667 52.906667 0 0 0-52.622222-52.280889H232.675556a52.622222 52.622222 0 0 0-52.622223 52.280889v517.233777a52.906667 52.906667 0 0 0 52.622223 52.280889h260.209777v51.825778H233.244444a104.96 104.96 0 0 1-105.244444-104.561778V275.228444C128 217.543111 175.217778 170.666667 233.244444 170.666667h415.061334z m19.342222 412.558222c77.824 0 148.138667 42.894222 183.808 111.843555a17.294222 17.294222 0 0 1 0 15.701334 206.165333 206.165333 0 0 1-183.864889 111.843555 205.994667 205.994667 0 0 1-183.808-112.014222 16.099556 16.099556 0 0 1 0-15.530667 206.165333 206.165333 0 0 1 183.808-111.843555z m0 42.894222c-61.838222 0-109.283556 23.722667-140.117333 76.743111 30.833778 52.963556 78.279111 76.743111 140.060444 76.743111 61.781333 0 109.226667-23.779556 140.060445-76.8-30.776889-52.963556-78.279111-76.686222-140.060445-76.686222z m-0.170667 42.609778c19.000889 0 34.474667 15.36 34.474667 34.247111a34.417778 34.417778 0 0 1-68.835556 0c0-18.887111 15.473778-34.247111 34.360889-34.247111zM466.944 533.276444a26.168889 26.168889 0 0 1 23.608889 12.572445 26.055111 26.055111 0 0 1-23.608889 39.253333h-156.444444a25.941333 25.941333 0 0 1 0-51.825778h156.444444z m156.273778-155.420444a26.168889 26.168889 0 0 1 23.608889 12.629333 26.055111 26.055111 0 0 1-23.608889 39.253334H310.499556a25.941333 25.941333 0 0 1 0-51.882667h312.718222z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-reparse" viewBox="0 0 1024 1024"><path d="M998.765714 523.629714c13.824 0 25.014857 11.190857 25.014857 25.014857a475.282286 475.282286 0 0 1-875.593142 256.219429l-27.574858 55.149714a25.014857 25.014857 0 1 1-44.763428-22.454857l44.178286-88.283428a24.868571 24.868571 0 0 1 26.550857-25.526858 25.014857 25.014857 0 0 1 8.265143 0.804572l99.474285 24.868571a25.014857 25.014857 0 0 1-12.068571 48.566857l-46.372572-11.556571A425.252571 425.252571 0 0 0 973.750857 548.571429c0-13.897143 11.190857-25.014857 25.014857-25.014858zM430.957714 365.714286l6.729143 0.658285c2.633143 0.438857 285.549714 160.109714 285.549714 160.109715 20.114286 17.846857 7.314286 34.523429-6.582857 45.933714-1.828571 1.462857-194.779429 113.078857-249.929143 144.969143l-10.678857 6.217143-3.876571 2.194285c-16.676571 8.923429-39.497143 8.923429-47.250286-11.995428-0.877714-2.194286-2.267429-250.221714-2.56-303.396572L402.285714 400.457143l0.731429-0.512c0.731429-18.651429 8.265143-38.034286 34.669714-33.645714z m-15.945143-273.408a475.282286 475.282286 0 0 1 533.869715 200.045714l27.501714-55.149714a25.014857 25.014857 0 1 1 44.690286 22.454857l-44.105143 88.283428a24.868571 24.868571 0 0 1-26.624 25.526858 24.868571 24.868571 0 0 1-8.192-0.804572l-99.547429-24.868571a25.014857 25.014857 0 0 1 12.068572-48.566857l46.445714 11.629714A425.252571 425.252571 0 0 0 123.245714 548.571429a25.014857 25.014857 0 0 1-50.029714 0 475.282286 475.282286 0 0 1 341.796571-456.265143z" ></path></symbol>' + + '<symbol id="icon-testing" viewBox="0 0 1024 1024"><path d="M720.704 525.824a202.432 202.432 0 0 1 158.656 328.128l69.888 69.952a29.952 29.952 0 0 1-41.728 42.88l-0.512-0.576-71.296-71.296A202.432 202.432 0 0 1 518.4 728.32a202.432 202.432 0 0 1 202.304-202.496z m32.192-464.832c62.848 0 113.92 50.496 114.944 113.152v240.832a29.888 29.888 0 0 1-59.776 0.704V176.064a55.232 55.232 0 0 0-54.272-55.232H168.96a55.232 55.232 0 0 0-55.168 54.336V824.96c0 30.208 24.192 54.72 54.272 55.232h303.872a29.888 29.888 0 0 1 0.64 59.776H168.96c-62.848 0-113.92-50.432-114.944-113.152V176.064c0-62.912 50.432-114.048 113.024-115.072h585.856z m-32.192 524.672a142.592 142.592 0 1 0 142.528 142.72c0-78.848-63.808-142.72-142.528-142.72z m-231.04-59.904a29.888 29.888 0 0 1 0.64 59.776H212.608a29.888 29.888 0 0 1-0.64-59.776H489.6z m153.664-233.536a29.888 29.888 0 0 1 0.704 59.84H212.608a29.888 29.888 0 0 1-0.64-59.776h431.36z" fill="#979AAB" ></path></symbol>' + + '<symbol id="icon-agent" viewBox="0 0 1024 1024"><path d="M512.8704 465.4592l343.4496-197.888a116.5824 116.5824 0 0 0-4.1984-2.6624l-287.0784-166.7072a103.7824 103.7824 0 0 0-103.8336-0.256L173.2096 263.168a71.5264 71.5264 0 0 0-4.1984 2.6112l343.8592 199.68z" fill="#757BF2" ></path><path d="M559.5648 943.7184l3.2768-1.7408 287.9488-165.2736a103.7824 103.7824 0 0 0 52.1216-89.8048l0.8192-331.9808c0-1.6384-0.1024-3.2768-0.1536-4.864l-344.064 198.2464v395.4176h0.0512zM464.4352 547.328L121.2416 347.9552a80.8448 80.8448 0 0 0-0.1536 5.0176l-0.8192 331.9808a103.7824 103.7824 0 0 0 51.712 90.0608l287.0784 166.7072c1.792 1.024 3.584 1.9968 5.4272 2.9184v-397.312h-0.0512z" fill="#6C6CEA" ></path><path d="M121.088 352.9728l-0.8192 331.9808a103.7824 103.7824 0 0 0 51.712 90.0608l93.184 54.1184a587.264 587.264 0 0 0 199.3728-3.6352V547.328L121.2416 347.9552a82.0736 82.0736 0 0 0-0.1536 5.0176z m438.4768 445.6448a583.8336 583.8336 0 0 0 343.9616-351.744l0.256-91.9552c0-1.6384-0.0512-3.2768-0.1536-4.864l-344.064 198.2464v250.3168z" fill="#757BF2" ></path><path d="M121.088 352.9728l-0.6656 271.4624c24.7808 3.1744 49.664 4.7616 74.6496 4.7616 97.2288 0 188.8768-23.8592 269.4144-65.9968v-15.8208L121.2416 347.9552a82.0736 82.0736 0 0 0-0.1536 5.0176z m558.848 16.1792a579.7888 579.7888 0 0 0 74.5984-160.9216l-189.5424-110.08a103.7824 103.7824 0 0 0-103.8336-0.256L173.2096 263.168a71.5264 71.5264 0 0 0-4.1984 2.6112l343.808 199.68L679.936 369.152z" fill="#8486F8" ></path><path d="M564.992 98.1504a103.7824 103.7824 0 0 0-103.8336-0.256L173.2096 263.168a71.5264 71.5264 0 0 0-4.1984 2.6112l153.3952 89.088a584.3456 584.3456 0 0 0 247.1936-254.0032l-4.608-2.7136z" fill="#8D92F8" ></path></symbol>' + + '<symbol id="icon-chat" viewBox="0 0 1024 1024"><path d="M790.72 242.944h-21.76a108 108 0 0 0-102.176-73.216H197.344A108.032 108.032 0 0 0 89.28 277.76v312.128a99.328 99.328 0 0 0 99.328 99.328c28.416 0 51.52 23.04 51.52 51.52v25.536c0 35.072 41.728 53.376 67.52 29.632l101.248-93.088a51.52 51.52 0 0 1 34.88-13.6h223.008a108.032 108.032 0 0 0 108.032-108.032v-253.568h15.872c32.16 0 58.24 26.176 58.24 58.272v290.656a50.24 50.24 0 0 1-50.144 50.176 90.4 90.4 0 0 0-90.336 90.336v12.736l-86.112-79.2a90.176 90.176 0 0 0-61.12-23.872h-110.496a42.4 42.4 0 0 0 0 84.672h110.496c1.376 0 2.752 0.544 3.776 1.504l94.272 86.688a79.52 79.52 0 0 0 53.76 21.248c11.104 0 22.016-2.304 32.384-6.848a78.784 78.784 0 0 0 47.776-73.12v-23.808c0-3.072 2.496-5.568 5.6-5.6a135.04 135.04 0 0 0 134.912-134.848v-290.656a143.04 143.04 0 0 0-142.976-143.008z m-277.632 309.856H273.984a39.616 39.616 0 1 1 0-79.264h239.104a39.616 39.616 0 1 1 0 79.264z m80.16-154.528H273.984a39.616 39.616 0 1 1 0-79.232h319.264a39.616 39.616 0 0 1 0 79.232z" fill="#6C6CEA" ></path><path d="M622.4 750.592a90.176 90.176 0 0 0-61.12-23.872h-110.56c-23.36 0-42.336 19.008-42.336 42.368 0 21.024 15.456 38.496 35.616 41.76a578.304 578.304 0 0 0 178.688-60l-0.32-0.256z" fill="#757BF2" ></path><path d="M923.808 333.664a143.104 143.104 0 0 0-133.088-90.72h-21.76a108 108 0 0 0-102.176-73.216H197.344A108.032 108.032 0 0 0 89.28 277.76v312.128a99.328 99.328 0 0 0 99.328 99.328c28.416 0 51.52 23.04 51.52 51.52v25.536c0 35.072 41.728 53.376 67.52 29.632l101.248-93.088a51.52 51.52 0 0 1 34.88-13.6h223.008a108.032 108.032 0 0 0 108.032-108.032v-253.568h15.872c32.16 0 58.24 26.176 58.24 58.272v150.4a576.64 576.64 0 0 0 74.88-202.624z m-410.72 219.136H273.984a39.616 39.616 0 1 1 0-79.264h239.104a39.616 39.616 0 1 1 0 79.264z m80.16-154.528H273.984a39.616 39.616 0 1 1 0-79.232h319.264a39.616 39.616 0 1 1 0 79.232z" fill="#757BF2" ></path><path d="M447.296 552.8H273.984a39.616 39.616 0 1 1 0-79.264h239.104c13.76 0 25.92 7.072 33.024 17.728a582.176 582.176 0 0 0 200.192-286.56 107.392 107.392 0 0 0-79.52-35.136H197.344A108.032 108.032 0 0 0 89.28 277.6v312.128c0 4.48 0.416 8.96 0.96 13.312 32.96 5.76 66.784 8.8 101.312 8.8 91.776 0.16 178.56-21.12 255.744-59.04zM273.984 319.072h319.264a39.616 39.616 0 1 1 0 79.264H273.984a39.616 39.616 0 1 1 0-79.264z" fill="#8486F8" ></path><path d="M237.376 373.824a39.616 39.616 0 0 1 36.608-54.72h77.696a585.184 585.184 0 0 0 161.76-149.376H197.312A108.032 108.032 0 0 0 89.28 277.76v126.88a579.2 579.2 0 0 0 148.096-30.816z" fill="#8D92F8" ></path></symbol>' + + '<symbol id="icon-CSV" viewBox="0 0 1024 1024"><path d="M530.368 0h55.424v95.36c98.88 0.64 197.76-1.408 297.216 0.64a28.672 28.672 0 0 1 26.752 11.648c7.04 8.768 10.432 21.12 9.216 33.536 1.536 234.688 0 469.44 0.896 704.192-0.896 24 1.856 50.688-8.96 72-13.568 12.48-31.04 11.264-46.848 11.264H585.984V1024h-58.112c-141.056-32.64-282.432-63.04-423.744-94.912V95.552C246.208 63.552 388.288 32.512 530.368 0z" fill="#D24625" ></path><path d="M585.792 131.008V893.44h305.728V131.008H585.728z m148.8 144.896c3.968 3.136 8.96 4.672 14.016 4.288a22.464 22.464 0 0 0 13.44-3.584 11.712 11.712 0 0 0 4.864-9.6c-0.256-5.504-3.072-10.24-7.168-12.224a145.984 145.984 0 0 0-14.976-6.336 117.12 117.12 0 0 1-17.984-8.128 22.72 22.72 0 0 1-9.024-19.52 24.384 24.384 0 0 1 8.96-20.16 34.112 34.112 0 0 1 41.728 0c5.632 5.76 8.96 14.336 8.96 23.424h-12.096a22.848 22.848 0 0 0-5.888-12.864 17.664 17.664 0 0 0-12.8-3.968 19.008 19.008 0 0 0-11.072 2.56c-3.072 1.856-4.992 5.888-4.736 10.112 0.128 4.288 2.24 8.128 5.376 9.792 4.224 2.368 8.576 4.352 13.12 6.016 7.04 2.368 13.76 5.568 20.288 9.472 6.144 4.096 9.984 12.16 9.92 20.864a26.688 26.688 0 0 1-8.064 20.544 38.912 38.912 0 0 1-44.992 0.64 36.48 36.48 0 0 1-10.944-27.52h12.608a24.96 24.96 0 0 0 6.4 16.192z m-96.64-32a63.808 63.808 0 0 1 8.96-34.944c6.848-10.496 17.344-16.256 28.16-15.36a30.272 30.272 0 0 1 22.4 8.96c5.44 5.888 8.96 14.272 9.728 23.296h-12.352a23.68 23.68 0 0 0-7.168-14.016 19.648 19.648 0 0 0-13.312-4.288c-7.04-0.512-13.76 3.328-18.048 10.432a50.56 50.56 0 0 0-5.632 25.92 51.712 51.712 0 0 0 5.568 26.368c8.384 11.136 21.888 13.184 32.128 4.864a31.68 31.68 0 0 0 7.68-16.512h12.352a46.144 46.144 0 0 1-12.032 26.496 28.928 28.928 0 0 1-21.44 8.96c-10.88 0.96-21.568-4.672-28.672-15.168a65.088 65.088 0 0 1-8.32-34.944z m187.968 48.256h-15.168l-27.008-96.64h13.696l21.12 79.04 21.12-79.104h13.632l-27.392 96.704z" fill="#FFFFFF" ></path><path d="M618.24 366.144h240.832v110.336H618.24V366.08z m0 164.736h240.832v110.4H618.24V530.88z m0 164.8h240.832v110.336H618.24V695.68z" fill="#D24625" ></path><path d="M408 584.256l-2.56 12.608c-3.2 20.032-11.52 38.208-23.68 51.712a57.92 57.92 0 0 1-41.6 15.168c-26.496 0-45.952-10.048-58.112-29.824-12.16-19.84-17.536-48.576-17.536-86.72 0-38.208 6.016-66.112 17.92-85.952a62.336 62.336 0 0 1 56.448-31.488 60.8 60.8 0 0 1 41.408 13.056c10.944 10.048 18.432 24.96 21.12 41.6l2.688 12.48h71.872L473.6 477.504c-3.648-37.824-18.752-72.32-42.24-96.576-26.176-25.344-58.624-38.272-91.584-36.608-49.664 0-88.128 21.248-114.304 62.848-22.4 35.648-34.496 82.88-34.496 140.032 0 57.152 11.2 104.768 33.152 139.328 25.92 41.472 65.088 62.464 116.864 62.464 31.488 0.768 62.08-12.16 86.848-36.608 26.496-27.2 44.032-65.664 49.344-108.032l3.072-20.16H408z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-data" viewBox="0 0 1024 1024"><path d="M148.576 419.488l345.344 160.16a67.712 67.712 0 0 0 58.624-0.672l336.864-165.472a67.744 67.744 0 0 0 38.016-60.672 67.712 67.712 0 0 0-37.248-61.12L556.48 122.432a68.32 68.32 0 0 0-61.28-0.16L146.72 296.864a67.616 67.616 0 0 0-37.568 61.888 67.52 67.52 0 0 0 39.424 60.736z m738.208 220.416L522.912 828.16 149.76 645.952a43.456 43.456 0 1 0-38.112 78.176l375.232 183.232a82.24 82.24 0 0 0 73.888-0.864l366.016-189.344a43.456 43.456 0 0 0 18.656-58.624 43.52 43.52 0 0 0-58.688-18.624z" fill="#FC7032" ></path><path d="M111.616 574.08l375.232 183.232a82.24 82.24 0 0 0 73.92-0.896l366.016-189.312a43.456 43.456 0 1 0-40-77.28L522.912 678.08 149.76 495.872a43.456 43.456 0 0 0-58.144 20.032 43.424 43.424 0 0 0 19.968 58.144z" fill="#FC7032" ></path><path d="M495.264 122.304L146.752 296.864A67.616 67.616 0 0 0 109.12 358.72c0.416 26.368 15.52 49.6 39.424 60.736l345.344 160.16a67.712 67.712 0 0 0 58.624-0.672l253.664-124.64a523.264 523.264 0 0 0 1.6-204.384l-251.36-127.488a68.096 68.096 0 0 0-61.184-0.16z" fill="#FF7C33" ></path><path d="M778.048 546.112l-255.2 131.968-373.024-182.208a43.456 43.456 0 1 0-38.144 78.176l375.232 183.264a82.24 82.24 0 0 0 73.888-0.896l127.936-66.176a519.552 519.552 0 0 0 89.28-144.128z m-266.88 276.32L149.824 645.952a43.456 43.456 0 1 0-38.144 78.176l278.72 136.096a516.48 516.48 0 0 0 120.768-37.792z" fill="#FF7C33" ></path><path d="M674.144 182.176L556.48 122.528a68.32 68.32 0 0 0-61.28-0.16L146.72 296.864a67.616 67.616 0 0 0-37.568 61.888c0.416 26.368 15.52 49.6 39.424 60.736l328 152.096a519.776 519.776 0 0 0 197.568-389.44z m-270.336 437.76l-253.984-124.064a43.456 43.456 0 1 0-38.144 78.176l184.256 90.016a518.464 518.464 0 0 0 107.872-44.16zM153.664 683.84c22.08 0 43.776-1.536 65.152-4.192l-68.992-33.696a43.456 43.456 0 0 0-62.208 33.632 540.8 540.8 0 0 0 66.048 4.256z" fill="#FF9552" ></path><path d="M495.264 122.304L146.752 296.864A67.616 67.616 0 0 0 109.12 358.72c0.416 26.368 15.52 49.6 39.424 60.736l80.608 37.376A522.112 522.112 0 0 0 525.376 115.2a68.096 68.096 0 0 0-30.112 7.104z" fill="#FFA56A" ></path></symbol>' + + '<symbol id="icon-doc" viewBox="0 0 1024 1024"><path d="M894.08 863.616H525.44c-14.336 0-25.92-14.08-25.92-31.36V193.92c0-17.344 11.584-31.424 25.856-31.424h368.64c14.272 0 25.856 14.08 25.856 31.36v638.272c0 17.344-11.584 31.36-25.856 31.36v0.064z" fill="#E8E8E8" ></path><path d="M788.672 353.28H525.44c-14.272 0-25.856-14.08-25.856-31.424s11.584-31.424 25.856-31.424h263.296c14.336 0 25.856 14.08 25.856 31.36 0 16.32-11.52 31.424-25.856 31.424v0.064z m0 127.808H525.44c-14.272 0-25.856-14.08-25.856-31.36 0-17.344 11.584-31.488 25.856-31.488h263.296c14.336 0 25.856 14.08 25.856 31.424 0 17.408-11.52 31.424-25.856 31.424z m0 126.848H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z m0 127.872H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z" fill="#B2B2B2" ></path><path d="M595.008 1024l-490.88-113.792V113.792L595.008 0z" fill="#0D47A1" ></path><path d="M138.24 409.6v204.8h58.432c25.92 0 45.44-9.152 58.88-27.52 12.736-17.472 19.2-42.432 19.2-74.88 0-32.64-6.464-57.664-19.2-74.88-13.44-18.368-32.96-27.52-58.88-27.52H138.24z m26.24 28.672h27.264c19.904 0 34.432 5.76 43.648 17.536 8.96 11.456 13.44 30.336 13.44 56.192 0 25.28-4.48 43.904-13.44 55.936-9.216 11.776-23.68 17.728-43.648 17.728h-27.328V438.272zM342.848 409.6c-21.568 0-38.464 9.6-50.688 29.504-11.776 18.816-17.472 43.072-17.472 73.152 0 29.824 5.76 54.144 17.472 72.896 12.16 19.328 29.12 29.248 50.688 29.248 21.44 0 38.336-9.6 50.688-28.992 11.776-18.432 17.728-42.752 17.728-73.152 0-30.336-5.952-54.912-17.728-73.344-12.352-19.648-29.248-29.312-50.688-29.312z m0 28.416c14.592 0 25.792 6.4 33.6 19.584 7.68 13.248 11.648 31.488 11.648 54.656 0 23.232-3.968 41.152-11.648 54.144a37.312 37.312 0 0 1-33.6 19.584c-14.528 0-25.92-6.912-33.792-20.48-7.68-13.184-11.392-30.848-11.392-53.184 0-22.656 3.776-40.32 11.392-53.632 8.064-13.76 19.264-20.672 33.792-20.672zM482.56 409.6a59.904 59.904 0 0 0-54.656 31.168c-11.072 18.24-16.512 42.24-16.512 71.488 0 29.824 5.184 53.568 15.872 71.232 12.16 20.48 30.784 30.912 55.68 30.912a55.104 55.104 0 0 0 41.536-18.24c12.352-12.928 20.096-30.848 23.424-54.08h-23.872c-2.944 14.912-8 26.24-15.104 33.664a35.968 35.968 0 0 1-26.176 10.24 39.296 39.296 0 0 1-36.16-20.16c-7.168-12.416-10.688-30.4-10.688-53.568 0-22.592 3.52-40.32 10.88-52.928a37.888 37.888 0 0 1 35.392-21.312 36.48 36.48 0 0 1 25.728 8.832c6.912 6.08 11.52 15.424 14.08 28.48h23.808c-2.304-19.904-8.768-35.904-19.648-47.488a56.96 56.96 0 0 0-43.52-18.24z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-excel" viewBox="0 0 1024 1024"><path d="M530.752 0h55.04v95.296c92.8 0 185.6 0.192 278.272-0.384 15.68 0.832 32.896-0.64 46.528 11.392 9.6 17.6 8.384 40.064 9.152 60.288-0.512 206.72-0.576 413.376-0.256 619.968-0.384 34.56 2.56 69.952-3.136 104.128-3.712 24.704-26.88 25.344-42.432 26.176-96 0.384-192.064-0.256-288.192 0V1024h-57.472c-141.248-32.96-282.752-63.488-424.128-95.296V95.36C246.336 63.552 388.608 32.192 530.752 0z" fill="#207245" ></path><path d="M585.856 130.944h305.792v750.208H585.792v-71.488h74.176v-83.328H585.856v-47.616h74.112V595.328H585.856v-47.616h74.112V464.384H585.856v-47.68h74.112V333.44H585.856v-47.616h74.112V202.368H585.856z" fill="#FFFFFF" ></path><path d="M696.96 202.432h129.728v83.328h-129.664V202.368z m0 130.944h129.728v83.328h-129.664V333.44z m0 131.008h129.728v83.328h-129.664V464.384z m0 130.944h129.728v83.392h-129.664V595.328z m0 131.008h129.728v83.392h-129.664v-83.392z" fill="#207245" ></path><path d="M137.216 484.608V576H204.16v-9.088h-56.448v-33.536h51.2v-9.088h-51.2v-30.592h54.272v-9.088H137.216z m75.392 0l31.232 43.904L210.176 576h12.928l27.136-39.168 27.264 39.168h12.928l-33.92-47.488 31.488-43.904h-12.928l-24.832 35.584-24.704-35.584h-12.928z m127.104-1.792c-14.08 0-25.088 4.736-32.768 14.464a51.392 51.392 0 0 0-10.496 33.408c0 13.568 3.328 24.576 10.24 33.024 7.552 9.344 18.432 14.08 32.64 14.08a39.872 39.872 0 0 0 25.216-8.32 39.68 39.68 0 0 0 14.336-24.832h-10.112a30.208 30.208 0 0 1-10.88 18.048 29.952 29.952 0 0 1-18.56 5.76 29.44 29.44 0 0 1-24.448-10.624c-5.376-6.656-7.936-15.744-7.936-27.136s2.688-20.48 8.064-27.392a29.248 29.248 0 0 1 24.576-11.136c7.168 0 13.184 1.664 18.176 5.12 5.12 3.584 8.448 8.704 9.728 15.36H377.6a32.192 32.192 0 0 0-12.288-21.76 40.512 40.512 0 0 0-25.6-8.064z m53.76 1.792V576h66.944v-9.088h-56.448v-33.536h51.2v-9.088h-51.2v-30.592h54.272v-9.088H393.472z m81.536 0V576h63.232v-9.088h-52.864V484.608h-10.368z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-md" viewBox="0 0 1024 1024"><path d="M533.376 0h52.544v107.2c99.072 0.64 198.208-1.344 297.216 0.576 21.312-2.624 38.016 18.688 35.84 46.08 1.6 222.4-0.32 444.928 0.896 667.52-0.896 24.064 1.856 50.688-8.832 72-13.568 12.608-31.168 10.944-47.04 11.968-92.672-0.64-185.344-0.384-278.08-0.384V1024h-57.664c-141.184-33.088-282.752-63.36-424.064-95.232-0.128-277.824-0.128-555.584 0-833.28C247.168 63.68 390.144 31.296 533.376 0z" fill="#20B2AA" ></path><path d="M582.848 142.912v354.176h115.84V512h-115.84v104.192h242.496v44.608h-242.56v56.576h242.56v44.672h-242.56v107.072h305.92V142.912h-305.92z m180.8 293.12a98.624 98.624 0 0 0-11.072 36.48c-0.256 3.584-2.112 6.72-4.736 8.064l-40 18.56-4.48-3.008 18.432-31.744c1.408 0.64 3.008 0.96 4.544 0.896 7.168-0.064 12.928-6.976 12.8-15.36 0-8.32-5.824-15.104-12.992-14.976-7.168 0.064-12.928 6.976-12.864 15.36 0 1.92 0.32 3.712 0.896 5.44a11.648 11.648 0 0 1-0.512 9.536l-15.616 26.944-3.712-4.48-0.128-49.792c0-3.776 1.6-7.168 4.224-8.832 6.4-3.84 18.432-12.608 27.648-26.752 2.496-3.712 6.72-4.416 9.856-1.664l25.6 21.568a10.24 10.24 0 0 1 3.264 6.336c0.448 2.56 0 5.12-1.152 7.424z m18.176-17.28c-2.368 4.032-6.72 4.992-10.048 2.368l-39.04-31.232a10.112 10.112 0 0 1-3.392-6.208 12.16 12.16 0 0 1 1.152-7.552l0.256-0.512c2.304-4.48 4.096-1.92 7.616 0.96l38.656 31.808c3.712 3.072 7.488 5.888 4.736 10.432h0.064z m33.024-62.912l-22.016 41.856c-3.648 6.912-11.008 8.768-16.384 4.096l-26.112-22.592c-5.376-4.736-6.784-14.144-3.2-21.12l22.016-41.856a12.096 12.096 0 0 1 7.616-6.4 9.792 9.792 0 0 1 8.832 2.304l26.048 22.656c5.376 4.736 6.848 14.08 3.2 21.12V355.84z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-jpg" viewBox="0 0 1024 1024"><path d="M533.312 0h52.544v107.136c99.072 0.64 198.272-1.28 297.216 0.576 21.312-2.56 38.016 18.752 35.84 46.08 1.6 222.464-0.256 444.992 0.96 667.52-0.896 24.064 1.856 50.752-8.96 72.064-13.44 12.608-31.104 11.008-46.976 11.968-92.672-0.64-185.344-0.384-278.08-0.384V1024h-57.664c-141.248-33.088-282.688-63.36-424-95.36-0.128-277.76-0.128-555.456 0-833.152C247.04 63.744 390.208 31.36 533.312 0z" fill="#077467" ></path><path d="M582.784 142.784v515.136l6.912-4.48 12.8-7.488 12.736-19.328h25.472l41.728-64 72.96 101.248h26.688l56.704 56.576-60.16-32.768h-22.08l-67.2-53.504-35.84 55.04-17.408-26.88-53.312-4.352v211.328h305.792V142.784H582.784z m206.144 287.36c-23.04 0-41.664-24-41.664-53.632 0-29.632 18.624-53.632 41.664-53.632 23.104 0 41.792 24 41.792 53.632v0.064c-0.128 29.632-18.688 53.632-41.792 53.568z m-599.808-16h23.168V508.16c0 12.288-0.768 21.632-2.56 28.288a40.96 40.96 0 0 1-12.224 20.864 33.92 33.92 0 0 1-23.488 7.808 31.04 31.04 0 0 1-26.24-12.288c-6.144-8.192-9.28-20.224-9.344-36.032l21.952-3.264c0.256 8.512 1.28 14.528 2.88 18.048 2.56 5.376 6.4 7.936 11.52 7.936 5.184 0 8.832-1.92 11.008-5.76 2.24-3.84 3.2-11.712 3.2-23.744V414.144h0.128z m48 148.288V414.08h37.376c14.144 0 23.36 0.768 27.648 2.304a30.72 30.72 0 0 1 16.64 14.464c4.416 7.552 6.592 17.088 6.592 28.928a60.16 60.16 0 0 1-3.84 22.912 38.592 38.592 0 0 1-9.792 14.72 31.168 31.168 0 0 1-12.16 7.04c-7.936 1.6-16 2.304-24.128 2.048h-15.232v55.936h-23.104v-0.064z m23.232-123.2v42.048h12.8c9.152 0 15.232-0.832 18.368-2.368a16.384 16.384 0 0 0 7.296-7.232 24.32 24.32 0 0 0 2.56-11.52 22.72 22.72 0 0 0-3.712-13.312 15.168 15.168 0 0 0-9.344-6.592 84.416 84.416 0 0 0-16.768-1.024h-11.2z m137.92 68.736v-24.96h50.176v59.008a63.744 63.744 0 0 1-21.184 16.064 63.36 63.36 0 0 1-28.16 6.912 52.288 52.288 0 0 1-31.552-9.728 58.88 58.88 0 0 1-20.224-27.84 111.04 111.04 0 0 1-6.72-39.488c0-15.488 2.56-29.12 7.552-41.216a59.52 59.52 0 0 1 22.144-27.648 48.768 48.768 0 0 1 27.648-7.36c14.272 0 25.472 3.84 33.472 11.584 8.064 7.68 13.248 18.368 15.616 32l-23.168 5.632a32.384 32.384 0 0 0-9.216-17.28 23.68 23.68 0 0 0-16.704-6.272 28.48 28.48 0 0 0-24.32 12.416c-6.08 8.32-9.024 20.608-9.024 36.864 0 17.536 2.944 30.72 9.088 39.552a27.904 27.904 0 0 0 23.936 13.248 32.512 32.512 0 0 0 14.72-3.776 50.944 50.944 0 0 0 12.608-8.896v-18.816h-26.688z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-pdf" viewBox="0 0 1024 1024"><path d="M533.312 0h52.544v107.2c99.072 0.64 198.208-1.344 297.216 0.576 21.248-2.624 37.952 18.688 35.84 46.08 1.6 222.4-0.384 444.928 0.896 667.52-0.896 24 1.856 50.688-8.896 72-13.504 12.608-31.104 10.88-46.976 11.904-92.672-0.64-185.344-0.32-278.08-0.32V1024h-57.6c-141.312-33.088-282.816-63.36-424.128-95.232V95.488C247.168 63.68 390.144 31.36 533.312 0z" fill="#F2554E" ></path><path d="M156.16 555.392v-156.16h34.304c12.928 0 21.44 0.768 25.344 2.304a30.08 30.08 0 0 1 15.232 15.36c4.096 7.808 6.144 17.92 6.144 30.336 0 9.6-1.216 17.664-3.584 24.192a42.24 42.24 0 0 1-8.96 15.424 27.52 27.52 0 0 1-11.072 7.36 83.2 83.2 0 0 1-22.144 2.24h-13.888v58.88H156.16z m21.376-129.792v44.352h11.648c8.384 0 14.016-0.832 16.832-2.432a16.512 16.512 0 0 0 6.656-7.68 28.8 28.8 0 0 0 2.432-12.16 26.624 26.624 0 0 0-3.392-14.08 14.464 14.464 0 0 0-8.512-6.912c-5.12-0.896-10.24-1.28-15.36-1.024h-10.304z m76.992-26.432h39.04c8.832 0 15.488 1.024 20.16 3.008 6.144 2.688 11.648 7.68 15.872 14.336 4.608 7.36 8.064 16 10.112 25.28 2.304 10.048 3.456 22.336 3.456 36.992 0 12.8-1.088 23.872-3.2 33.216-2.688 11.392-6.464 20.544-11.328 27.584a36.8 36.8 0 0 1-15.04 12.48 44.8 44.8 0 0 1-18.88 3.328h-40.192v-156.16z m21.376 26.368v103.552h15.936a40.32 40.32 0 0 0 12.928-1.536c3.456-1.28 6.4-3.52 8.576-6.464a39.36 39.36 0 0 0 5.568-15.104c1.472-6.976 2.176-16.512 2.176-28.608 0-12.032-0.64-21.312-2.176-27.776a41.216 41.216 0 0 0-6.016-15.104 19.136 19.136 0 0 0-9.92-7.296 64.576 64.576 0 0 0-17.472-1.536l-9.6-0.064z m85.504 129.856v-156.16h72.448v26.304H382.72v36.992h44.16v26.368h-44.16v66.496h-21.312z m454.272 51.392a22.656 22.656 0 0 0-2.752-3.2c-4.096-3.968-12.48-9.792-28.928-14.784a139.008 139.008 0 0 0-5.12-1.472c10.944 10.496 20.288 16.96 27.968 19.2 4.544 1.472 7.36 0.896 8.832 0.256zM721.92 394.88c2.112-9.6 3.584-26.88 0.448-30.4v0.064c-4.16 7.36-9.408 37.504-7.68 55.68 3.264-7.488 4.352-12.48 7.168-25.344z m0.512 161.792a270.72 270.72 0 0 1-23.296-59.456 604.48 604.48 0 0 1-39.68 61.568c20.864-2.24 41.92-2.944 62.976-2.176z" fill="#FFFFFF" ></path><path d="M590.08 156.16v424.96c8.32-3.2 14.016-4.8 14.016-4.8 4.928-6.912 4.352-6.272 10.304-14.784 18.88-27.328 36.48-56.128 52.672-86.4-0.192-2.688-5.888-106.88 18.816-120.768a12.608 12.608 0 0 1 10.24-1.024 19.456 19.456 0 0 1 10.944 9.92c5.568 11.84 5.504 29.12 0.896 53.248-7.296 38.208-18.368 59.52-18.368 59.52s3.52 47.168 36.864 99.584c15.36 1.664 28.544 4.288 39.232 7.68 18.368 5.888 29.312 14.08 33.472 24.896a34.816 34.816 0 0 1-0.192 22.72c-2.752 8.96-5.568 13.376-10.688 16-4.288 2.176-9.664 2.368-15.936 0.32-18.752-6.016-43.52-29.824-59.712-49.024a597.568 597.568 0 0 0-94.464-0.896 508.928 508.928 0 0 1-28.16 37.376v250.432h295.04V156.16H590.208z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-png" viewBox="0 0 1024 1024"><path d="M586.24 141.952h305.792v726.656H586.24z" fill="#FFFFFF" ></path><path d="M918.912 153.92a47.424 47.424 0 0 0-9.088-33.664 28.928 28.928 0 0 0-26.752-11.84c-98.944-1.92-198.144 0-297.408 0V0h-52.352C390.144 31.36 247.04 63.744 104.064 95.552v833.6c141.44 32 282.88 62.208 424.128 94.848h57.6v-118.656h278.144c15.936-0.96 33.472 0.64 47.04-12.032 10.688-21.312 7.936-48.192 8.96-72.32-1.408-221.888 0.64-444.544-1.024-667.008z m-27.2 715.648h-305.92V143.04h305.92v726.528z" fill="#0071C5" ></path><path d="M658.368 244.16c19.84 0 30.08 10.624 30.08 32 0 21.248-10.24 32.128-30.4 32.128h-20.48v39.872H624V244.16h34.304z m-20.8 49.536h19.776a18.752 18.752 0 0 0 12.8-4.544 18.56 18.56 0 0 0 4.16-13.504 17.536 17.536 0 0 0-4.288-13.248 17.92 17.92 0 0 0-13.056-4.096h-19.392v35.392z m75.968-49.536l40.96 74.944v-74.88h13.696V348.16h-12.8l-41.344-76.032V348.16h-14.016V244.16h13.568z m132.352 6.912a42.24 42.24 0 0 1 11.136 26.048h-13.632a24.704 24.704 0 0 0-7.488-14.976 21.888 21.888 0 0 0-15.104-4.928 20.8 20.8 0 0 0-18.432 9.856 50.048 50.048 0 0 0-7.616 29.504c-0.512 10.24 1.856 20.48 6.72 28.672 5.824 7.552 14.08 11.392 22.4 10.496a33.472 33.472 0 0 0 11.136-1.664 37.056 37.056 0 0 0 8.896-4.8v-21.952h-21.952v-14.464h35.52v44.416a47.424 47.424 0 0 1-15.104 9.92 55.424 55.424 0 0 1-20.032 3.456 35.008 35.008 0 0 1-30.72-15.744 64.192 64.192 0 0 1-10.24-37.76c-0.512-14.08 3.2-27.712 10.24-38.464a33.088 33.088 0 0 1 28.8-16.064 33.856 33.856 0 0 1 25.472 8.448z m-204.032 405.504h196.608v147.776h-196.608v-147.84z" fill="#0071C5" ></path><path d="M249.92 341.12c50.304 3.072 111.36-25.6 153.472 22.784 39.488 62.464 28.928 177.024-30.72 214.4-20.992 13.824-45.056 11.968-67.968 10.88v139.776l-54.912-6.144c-0.768-127.104-0.96-254.464 0.128-381.76z" fill="#FFFFFF" ></path><path d="M304.96 405.504c18.24-0.96 40.832-5.312 53.248 16.704 10.048 24.448 10.496 53.376 1.152 78.208-10.56 24.128-34.432 22.08-52.864 24.96a2089.408 2089.408 0 0 1-1.536-119.872z m533.952-3.84H641.28v204.8h197.632v-204.8z m-181.312 19.968h165.12v124.8l-24.896-31.936a15.232 15.232 0 0 0-5.888-4.928c-4.288-1.536-13.312 9.408-27.392 33.024-17.728-22.784-56.064-71.168-64-69.76-2.24 0-5.632 2.176-10.304 7.232l-32.64 36.16V421.632z" fill="#0071C5" ></path><path d="M778.24 472.96c4.352 5.12 10.688 6.528 16.128 3.648 5.44-2.88 9.024-9.6 9.152-17.024a20.928 20.928 0 0 0-4.288-12.928 13.248 13.248 0 0 0-10.24-5.376c-6.016 0.128-11.328 4.608-13.632 11.52a22.528 22.528 0 0 0 2.944 20.16z" fill="#0071C5" ></path></symbol>' + + '<symbol id="icon-text" viewBox="0 0 1024 1024"><path d="M919.872 140.8c0-12.8-2.56-25.6-10.24-32a32.896 32.896 0 0 0-25.472-12.8c-99.392-3.2-198.784 0-298.24 0V0h-56.064C387.072 32 246.848 64 104.064 96v832c140.224 32 283.008 60.8 423.232 96h58.624v-96h277.824c15.36 0 33.216 0 45.952-12.8 10.24-22.4 7.68-48 10.24-73.6V140.8zM496.704 358.4v83.2H376.896v304H292.736V441.6H175.424V336H496.64v22.4z m361.984 483.2H585.92V192h272.768v649.6z" fill="#7A85B0" ></path><path d="M647.936 325.76h148.352v80.704h-148.352v-80.64z m0 164.608h148.352v80.704h-148.352v-80.64z m0 158.144h148.352v80.704h-148.352v-80.64z" fill="#7A85B0" ></path></symbol>' + + '<symbol id="icon-ppt" viewBox="0 0 1024 1024"><path d="M536.32 70.528h57.408v91.072c108.16 0.64 216.512-1.088 324.672 0.512a35.072 35.072 0 0 1 39.168 39.168c1.728 189.12-0.448 378.368 0.96 567.552-0.96 20.48 2.048 43.136-9.728 61.248-14.72 10.752-33.92 9.344-51.264 10.176-101.248-0.512-202.496-0.32-303.872-0.32v101.248h-62.976c-154.24-28.16-308.864-53.888-463.232-81.024-0.128-236.16-0.128-472.32 0-708.48 156.16-27.008 312.448-54.592 468.864-81.152z" fill="#D24625" ></path><path d="M593.728 192h334.08v617.6h-334.08v-81.024h242.944v-40.512H593.728v-50.56h242.944v-40.576H593.792l-0.192-59.52c40.064 12.48 85.76 12.16 121.6-11.968 38.784-23.04 59.008-66.816 62.336-110.592-44.416-0.256-88.896-0.192-133.184-0.192-0.128-43.968 0.448-88.064-0.96-131.968a2496 2496 0 0 0-49.664 10.24V192z" fill="#FFFFFF" ></path><path d="M664.64 261.568c70.4 3.2 129.536 62.464 133.248 132.608-44.416 0.512-88.896 0.32-133.312 0.32 0-44.352-0.128-88.704 0.064-132.928z m-374.656 131.2c19.968-0.896 44.672-4.608 58.112 14.144 11.52 19.84 10.88 46.016 1.28 66.432-11.52 20.864-37.632 18.816-57.792 21.248a1405.44 1405.44 0 0 1-1.6-101.824z" fill="#D24625" ></path><path d="M114.304 544.832V421.12h37.12c14.08 0 23.232 0.64 27.52 1.856a29.952 29.952 0 0 1 16.512 12.16c4.416 6.144 6.656 14.208 6.656 24.064 0 7.552-1.28 13.952-3.84 19.136a33.28 33.28 0 0 1-21.76 18.048 123.712 123.712 0 0 1-24 1.792h-15.04v46.72h-23.168z m23.168-102.848v35.136h12.672c9.088 0 15.168-0.64 18.24-1.92a15.36 15.36 0 0 0 7.232-6.08 17.6 17.6 0 0 0 2.624-9.6 16.768 16.768 0 0 0-3.648-11.2 15.68 15.68 0 0 0-9.344-5.504 105.216 105.216 0 0 0-16.64-0.832h-11.136z m83.584 102.848V421.12h37.12c14.08 0 23.232 0.64 27.52 1.856a29.952 29.952 0 0 1 16.448 12.16c4.48 6.144 6.656 14.208 6.656 24.064 0 7.552-1.28 13.952-3.84 19.136a33.28 33.28 0 0 1-21.76 18.048 123.712 123.712 0 0 1-23.936 1.792h-15.104v46.72h-23.04z m23.104-102.848v35.136h12.672c9.152 0 15.232-0.64 18.304-1.92a15.36 15.36 0 0 0 7.232-6.08 17.6 17.6 0 0 0 2.56-9.6 16.768 16.768 0 0 0-3.648-11.2 15.68 15.68 0 0 0-9.28-5.504 105.216 105.216 0 0 0-16.64-0.832h-11.2z m109.44 102.848V441.984h-34.048V421.12h91.008v20.864h-33.92v102.784l-23.04 0.064z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-search" viewBox="0 0 1024 1024"><path d="M380.928 497.28a117.568 117.568 0 1 0 235.104 0 117.568 117.568 0 0 0-235.104 0z" fill="#1DCE75" ></path><path d="M750.016 121.792H275.296a150.496 150.496 0 0 0-150.304 150.336V752.64a150.496 150.496 0 0 0 150.304 150.336h474.72a150.496 150.496 0 0 0 150.336-150.336V272.128a150.496 150.496 0 0 0-150.336-150.336z m-17.536 611.392a30.368 30.368 0 0 1-42.912-0.256l-79.584-80.64a189.696 189.696 0 0 1-111.488 36.032 191.296 191.296 0 0 1-191.104-191.04 191.296 191.296 0 0 1 191.104-191.104 191.296 191.296 0 0 1 191.04 191.072c0 41.92-13.6 80.704-36.544 112.224l79.776 80.8a30.4 30.4 0 0 1-0.32 42.912z" fill="#05C46D" ></path><path d="M750.016 121.792H275.296a150.496 150.496 0 0 0-150.304 150.336V752.64a150.496 150.496 0 0 0 150.304 150.336h81.28a612.48 612.48 0 0 0 344.608-162.88 29.92 29.92 0 0 1-11.616-7.232l-79.584-80.64a189.696 189.696 0 0 1-111.488 36.064 191.296 191.296 0 0 1-191.104-191.072 191.232 191.232 0 0 1 191.104-191.04 191.296 191.296 0 0 1 191.04 191.072c0 41.92-13.6 80.704-36.544 112.224l79.776 80.8a30.08 30.08 0 0 1 6.656 10.336 612.352 612.352 0 0 0 154.56-407.488c0-26.048-1.6-52.032-4.896-77.888a150.464 150.464 0 0 0-139.072-93.44z" fill="#1DCE75" ></path><path d="M602.976 443.584a117.6 117.6 0 0 0-104.48-63.872 117.696 117.696 0 0 0-117.568 117.536 117.44 117.44 0 0 0 55.136 99.424 619.776 619.776 0 0 0 166.912-153.088z" fill="#3CD38E" ></path><path d="M124.928 272.128v417.184a612.16 612.16 0 0 0 240.736-54.848 190.592 190.592 0 0 1-58.304-137.216 191.296 191.296 0 0 1 191.072-191.072c59.712 0 113.088 27.52 148.16 70.592a610.56 610.56 0 0 0 77.44-254.976H275.296c-82.88 0-150.368 67.456-150.368 150.336z" fill="#3CD38E" ></path><path d="M124.928 272.128v176.48A616.288 616.288 0 0 0 511.552 121.792H275.296c-82.88 0-150.368 67.456-150.368 150.336z" fill="#48E1AA" ></path></symbol>' + + '<symbol id="icon-system" viewBox="0 0 1024 1024"><path d="M764.416 97.472H259.648a106.752 106.752 0 0 0-106.752 106.752v664a54.72 54.72 0 0 0 78.08 49.536l235.104-111.008a106.688 106.688 0 0 1 91.04-0.032l235.904 111.104a54.72 54.72 0 0 0 78.048-49.536V204.224a106.656 106.656 0 0 0-106.656-106.752zM671.68 577.856h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 1 1 0 76.288z m0-174.144h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 0 1 0 76.288z" fill="#2595E8" ></path><path d="M871.104 469.824v-265.6a106.752 106.752 0 0 0-106.752-106.752H259.648a106.752 106.752 0 0 0-106.752 106.752V838.4a635.296 635.296 0 0 0 229.824 7.68l83.36-39.36a106.784 106.784 0 0 1 74.88-5.952 635.84 635.84 0 0 0 330.144-330.944z m-199.424 108.032h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 1 1 0 76.288z m0-174.144h-319.36a38.144 38.144 0 1 1 0-76.288h319.36a38.144 38.144 0 0 1 0 76.288z" fill="#3A9CED" ></path><path d="M362.592 577.856h-10.272a38.144 38.144 0 0 1 0-76.288h142.848a637.12 637.12 0 0 0 103.808-97.856H352.32a38.144 38.144 0 1 1 0-76.288h302.208a630.496 630.496 0 0 0 86.272-229.888H259.648a106.752 106.752 0 0 0-106.752 106.752v422.496a631.072 631.072 0 0 0 209.696-48.96z" fill="#59ADF8" ></path><path d="M498.496 97.472H259.648a106.752 106.752 0 0 0-106.752 106.752v168.064a635.488 635.488 0 0 0 345.6-274.816z" fill="#6BC2FC" ></path></symbol>' + + '<symbol id="icon-word" viewBox="0 0 1024 1024"><path d="M894.08 863.616H525.44c-14.336 0-25.92-14.08-25.92-31.36V193.92c0-17.344 11.584-31.424 25.856-31.424h368.64c14.272 0 25.856 14.08 25.856 31.36v638.272c0 17.344-11.584 31.36-25.856 31.36v0.064z" fill="#E8E8E8" ></path><path d="M788.672 353.28H525.44c-14.272 0-25.856-14.08-25.856-31.424s11.584-31.424 25.856-31.424h263.296c14.336 0 25.856 14.08 25.856 31.36 0 16.32-11.52 31.424-25.856 31.424v0.064z m0 127.808H525.44c-14.272 0-25.856-14.08-25.856-31.36 0-17.344 11.584-31.488 25.856-31.488h263.296c14.336 0 25.856 14.08 25.856 31.424 0 17.408-11.52 31.424-25.856 31.424z m0 126.848H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z m0 127.872H525.44c-14.272 0-25.856-14.08-25.856-31.488 0-17.28 11.584-31.36 25.856-31.36h263.296c14.336 0 25.856 14.08 25.856 31.36 0 17.344-11.52 31.424-25.856 31.424z" fill="#B2B2B2" ></path><path d="M595.008 1024l-490.88-113.792V113.792L595.008 0z" fill="#0D47A1" ></path><path d="M455.808 707.584h-62.464l-41.152-250.24a236.8 236.8 0 0 1-3.52-43.392h-0.896a413.44 413.44 0 0 1-4.48 43.328l-42.88 250.304H235.328L170.24 317.504h61.568l34.816 260.096c1.792 10.816 2.688 25.984 3.584 44.352h0.896c0-14.08 2.688-29.248 5.376-45.44l44.608-259.008h59.776l41.088 262.208a371.2 371.2 0 0 1 3.584 42.24h0.896c0.896-14.08 1.792-28.16 3.52-43.328l34.816-260.032h56.256l-65.152 388.992z" fill="#FFFFFF" ></path></symbol>' + + '<symbol id="icon-file" viewBox="0 0 1024 1024"><path d="M181.12 493.664a127.168 127.168 0 0 1 114.56-71.04h548.352v-30.592a99.2 99.2 0 0 0-99.2-99.2H601.6c-23.36 0-45.952-8.256-63.84-23.264l-111.424-93.696a99.328 99.328 0 0 0-63.84-23.232H185.952a99.2 99.2 0 0 0-99.2 99.2v432l94.336-190.176z" fill="#2595E8" ></path><path d="M879.008 487.936H295.68c-23.744 0-45.504 13.44-56.064 34.752l-143.264 288.768a99.2 99.2 0 0 0 89.6 56.8h558.816a99.328 99.328 0 0 0 90.656-58.848l0.096 0.128 100.928-234.24a62.56 62.56 0 0 0-57.44-87.36z m-176.992 309.92h-186.56a37.984 37.984 0 1 1 0-76h186.56a37.984 37.984 0 1 1 0 76z" fill="#2595E8" ></path><path d="M800.352 333.728a633.6 633.6 0 0 0-0.512-24.288 98.752 98.752 0 0 0-55.072-16.672H601.6c-23.36 0-45.952-8.256-63.84-23.264l-111.424-93.632a99.328 99.328 0 0 0-63.84-23.232H185.952a99.2 99.2 0 0 0-99.2 99.2v432l94.336-190.208a127.168 127.168 0 0 1 114.592-71.072h498.24c4.224-28.992 6.432-58.688 6.432-88.832z" fill="#3A9CED" ></path><path d="M295.68 487.936c-23.744 0-45.504 13.44-56.064 34.752l-143.264 288.768a99.2 99.2 0 0 0 89.6 56.8h296.64a609.92 609.92 0 0 0 102.624-70.4H515.52a37.984 37.984 0 1 1 0-76h144.96a606.144 606.144 0 0 0 120.224-233.92H295.68z" fill="#3A9CED" ></path><path d="M608.416 292.768H601.6c-23.36 0-45.952-8.256-63.84-23.264l-111.424-93.632a99.328 99.328 0 0 0-63.84-23.232H185.952a99.2 99.2 0 0 0-99.2 99.2v432l94.336-190.208a127.168 127.168 0 0 1 114.592-71.072h256.512a607.136 607.136 0 0 0 56.224-129.792z" fill="#59ADF8" ></path><path d="M239.616 522.688l-95.072 191.616a608.256 608.256 0 0 0 363.84-226.432H295.68a62.656 62.656 0 0 0-56.064 34.816z" fill="#59ADF8" ></path><path d="M418.976 170.304a99.2 99.2 0 0 0-56.544-17.664H185.952a99.2 99.2 0 0 0-99.2 99.2v220.128a610.144 610.144 0 0 0 332.224-301.664z" fill="#6BC2FC" ></path></symbol>' + + `<symbol id="icon-a-DiscordIconSVGVectorIcon" viewBox="0 0 1024 1024"> + <path d="M867.424 180.388A834.008 834.008 0 0 0 656.168 114c-9.1 16.452-19.732 38.58-27.064 56.184-78.768-11.844-156.812-11.844-234.132 0-7.328-17.6-18.2-39.732-27.384-56.184a831.236 831.236 0 0 0-211.42 66.552C22.472 382.588-13.772 579.6 4.348 773.824c88.676 66.22 174.612 106.448 259.1 132.772A644.376 644.376 0 0 0 318.94 815.2a545.652 545.652 0 0 1-87.384-42.528 434.544 434.544 0 0 0 21.424-16.948c168.488 78.808 351.56 78.808 518.04 0a526.64 526.64 0 0 0 21.42 16.948 544.28 544.28 0 0 1-87.544 42.612c16.024 32.08 34.552 62.68 55.492 91.392 84.568-26.32 170.584-66.548 259.26-132.852 21.264-225.152-36.32-420.36-152.224-593.44zM341.896 654.38c-50.58 0-92.06-47.22-92.06-104.72s40.596-104.8 92.06-104.8c51.468 0 92.944 47.216 92.06 104.8 0.08 57.5-40.592 104.72-92.06 104.72z m340.204 0c-50.58 0-92.056-47.22-92.056-104.72s40.592-104.8 92.056-104.8c51.468 0 92.944 47.216 92.06 104.8 0 57.5-40.592 104.72-92.06 104.72z"></path> + </symbol>` + + `<symbol id="icon-GitHub" viewBox="0 0 1024 1024"> + <path d="M512 42.666667C252.714667 42.666667 42.666667 252.714667 42.666667 512c0 207.658667 134.357333 383.104 320.896 445.269333 23.466667 4.096 32.256-9.941333 32.256-22.272 0-11.178667-0.554667-48.128-0.554667-87.424-117.930667 21.717333-148.437333-28.757333-157.824-55.125333-5.290667-13.525333-28.16-55.168-48.085333-66.304-16.426667-8.832-39.936-30.506667-0.597334-31.104 36.949333-0.597333 63.36 34.005333 72.149334 48.128 42.24 70.954667 109.696 51.029333 136.704 38.698667 4.096-30.506667 16.426667-51.029333 29.909333-62.762667-104.448-11.733333-213.546667-52.224-213.546667-231.765333 0-51.029333 18.176-93.269333 48.128-126.122667-4.736-11.733333-21.162667-59.818667 4.693334-124.373333 0 0 39.296-12.288 129.024 48.128a434.901333 434.901333 0 0 1 117.333333-15.829334c39.936 0 79.829333 5.248 117.333333 15.829334 89.770667-61.013333 129.066667-48.128 129.066667-48.128 25.813333 64.554667 9.429333 112.64 4.736 124.373333 29.909333 32.853333 48.085333 74.538667 48.085333 126.122667 0 180.138667-109.696 220.032-214.144 231.765333 17.024 14.677333 31.701333 42.837333 31.701334 86.826667 0 62.762667-0.597333 113.237333-0.597334 129.066666 0 12.330667 8.789333 26.965333 32.256 22.272C846.976 895.104 981.333333 719.104 981.333333 512c0-259.285333-210.005333-469.333333-469.333333-469.333333z"></path> + </symbol>` + + '</svg>'), ((h) => { var a = (l = (l = document.getElementsByTagName('script'))[ l.length - 1 diff --git a/web/src/assets/svg/data-flow/data-icon-bri.svg b/web/src/assets/svg/data-flow/data-icon-bri.svg new file mode 100644 index 000000000..355ea9090 --- /dev/null +++ b/web/src/assets/svg/data-flow/data-icon-bri.svg @@ -0,0 +1,15 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M35.3194 10.6367H20.4258C19.4857 10.6367 18.7236 11.3988 18.7236 12.3388V34.892C18.7236 35.8321 19.4857 36.5942 20.4258 36.5942H35.3194C36.2594 36.5942 37.0215 35.8321 37.0215 34.892V12.3388C37.0215 11.3988 36.2594 10.6367 35.3194 10.6367Z" fill="url(#paint0_linear_488_37636)"/> +<path d="M31.0639 4.25391H5.10642C4.16637 4.25391 3.4043 5.01597 3.4043 5.95603V18.2965C3.4043 19.2365 4.16637 19.9986 5.10642 19.9986H31.0639C32.0039 19.9986 32.766 19.2365 32.766 18.2965V5.95603C32.766 5.01597 32.0039 4.25391 31.0639 4.25391Z" fill="#00BEB4" fill-opacity="0.1"/> +<path d="M31.0639 4.25391C32.0039 4.25391 32.766 5.01597 32.766 5.95603V18.2965C32.766 19.2365 32.0039 19.9986 31.0639 19.9986H5.10642C4.16637 19.9986 3.4043 19.2365 3.4043 18.2965V5.95603C3.4043 5.01597 4.16637 4.25391 5.10642 4.25391H31.0639ZM31.0639 4.67944H5.10642C4.40138 4.67944 3.82983 5.25099 3.82983 5.95603V18.2965C3.82983 19.0015 4.40138 19.5731 5.10642 19.5731H31.0639C31.7689 19.5731 32.3405 19.0015 32.3405 18.2965V5.95603C32.3405 5.25099 31.7689 4.67944 31.0639 4.67944Z" fill="#00BEB4"/> +<path d="M31.0639 22.5547H5.10642C4.16637 22.5547 3.4043 23.3168 3.4043 24.2568V34.8951C3.4043 35.8352 4.16637 36.5972 5.10642 36.5972H31.0639C32.0039 36.5972 32.766 35.8352 32.766 34.8951V24.2568C32.766 23.3168 32.0039 22.5547 31.0639 22.5547Z" fill="#00BEB4" fill-opacity="0.1"/> +<path d="M31.0639 22.5547C32.0039 22.5547 32.766 23.3168 32.766 24.2568V34.8951C32.766 35.8352 32.0039 36.5972 31.0639 36.5972H5.10642C4.16637 36.5972 3.4043 35.8352 3.4043 34.8951V24.2568C3.4043 23.3168 4.16637 22.5547 5.10642 22.5547H31.0639ZM31.0639 22.9802H5.10642C4.40138 22.9802 3.82983 23.5518 3.82983 24.2568V34.8951C3.82983 35.6002 4.40138 36.1717 5.10642 36.1717H31.0639C31.7689 36.1717 32.3405 35.6002 32.3405 34.8951V24.2568C32.3405 23.5518 31.7689 22.9802 31.0639 22.9802Z" fill="#00BEB4"/> +<path d="M10.6384 14.8949C12.2835 14.8949 13.6171 13.5613 13.6171 11.9162C13.6171 10.2711 12.2835 8.9375 10.6384 8.9375C8.99329 8.9375 7.65967 10.2711 7.65967 11.9162C7.65967 13.5613 8.99329 14.8949 10.6384 14.8949Z" fill="#00BEB4"/> +<path d="M10.6384 32.766C12.2835 32.766 13.6171 31.4324 13.6171 29.7873C13.6171 28.1422 12.2835 26.8086 10.6384 26.8086C8.99329 26.8086 7.65967 28.1422 7.65967 29.7873C7.65967 31.4324 8.99329 32.766 10.6384 32.766Z" fill="#00BEB4"/> +<defs> +<linearGradient id="paint0_linear_488_37636" x1="933.617" y1="10.6367" x2="933.617" y2="2606.38" gradientUnits="userSpaceOnUse"> +<stop stop-color="#C9F1EF"/> +<stop offset="1" stop-color="#00BEB4"/> +</linearGradient> +</defs> +</svg> diff --git a/web/src/assets/svg/data-flow/data-icon.svg b/web/src/assets/svg/data-flow/data-icon.svg new file mode 100644 index 000000000..eddb6a3ba --- /dev/null +++ b/web/src/assets/svg/data-flow/data-icon.svg @@ -0,0 +1,15 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M35.3194 10.6387H20.4258C19.4857 10.6387 18.7236 11.4007 18.7236 12.3408V34.894C18.7236 35.834 19.4857 36.5961 20.4258 36.5961H35.3194C36.2594 36.5961 37.0215 35.834 37.0215 34.894V12.3408C37.0215 11.4007 36.2594 10.6387 35.3194 10.6387Z" fill="url(#paint0_linear_491_41413)"/> +<path d="M31.0639 4.25586H5.10642C4.16637 4.25586 3.4043 5.01793 3.4043 5.95799V18.2984C3.4043 19.2385 4.16637 20.0005 5.10642 20.0005H31.0639C32.0039 20.0005 32.766 19.2385 32.766 18.2984V5.95799C32.766 5.01793 32.0039 4.25586 31.0639 4.25586Z" fill="#00BEB4" fill-opacity="0.2"/> +<path d="M31.0639 4.25586C32.0039 4.25586 32.766 5.01793 32.766 5.95799V18.2984C32.766 19.2385 32.0039 20.0005 31.0639 20.0005H5.10642C4.16637 20.0005 3.4043 19.2385 3.4043 18.2984V5.95799C3.4043 5.01793 4.16637 4.25586 5.10642 4.25586H31.0639ZM31.0639 4.68139H5.10642C4.40138 4.68139 3.82983 5.25294 3.82983 5.95799V18.2984C3.82983 19.0035 4.40138 19.575 5.10642 19.575H31.0639C31.7689 19.575 32.3405 19.0035 32.3405 18.2984V5.95799C32.3405 5.25294 31.7689 4.68139 31.0639 4.68139Z" fill="#226365"/> +<path d="M31.0639 22.5527H5.10642C4.16637 22.5527 3.4043 23.3148 3.4043 24.2549V34.8932C3.4043 35.8332 4.16637 36.5953 5.10642 36.5953H31.0639C32.0039 36.5953 32.766 35.8332 32.766 34.8932V24.2549C32.766 23.3148 32.0039 22.5527 31.0639 22.5527Z" fill="#3A9093" fill-opacity="0.2"/> +<path d="M31.0639 22.5527C32.0039 22.5527 32.766 23.3148 32.766 24.2549V34.8932C32.766 35.8332 32.0039 36.5953 31.0639 36.5953H5.10642C4.16637 36.5953 3.4043 35.8332 3.4043 34.8932V24.2549C3.4043 23.3148 4.16637 22.5527 5.10642 22.5527H31.0639ZM31.0639 22.9783H5.10642C4.40138 22.9783 3.82983 23.5498 3.82983 24.2549V34.8932C3.82983 35.5982 4.40138 36.1698 5.10642 36.1698H31.0639C31.7689 36.1698 32.3405 35.5982 32.3405 34.8932V24.2549C32.3405 23.5498 31.7689 22.9783 31.0639 22.9783Z" fill="#226365"/> +<path d="M10.6384 14.893C12.2835 14.893 13.6171 13.5594 13.6171 11.9143C13.6171 10.2692 12.2835 8.93555 10.6384 8.93555C8.99329 8.93555 7.65967 10.2692 7.65967 11.9143C7.65967 13.5594 8.99329 14.893 10.6384 14.893Z" fill="#3A9093"/> +<path d="M10.6384 32.766C12.2835 32.766 13.6171 31.4324 13.6171 29.7873C13.6171 28.1422 12.2835 26.8086 10.6384 26.8086C8.99329 26.8086 7.65967 28.1422 7.65967 29.7873C7.65967 31.4324 8.99329 32.766 10.6384 32.766Z" fill="#3A9093"/> +<defs> +<linearGradient id="paint0_linear_491_41413" x1="933.617" y1="10.6387" x2="933.617" y2="2606.38" gradientUnits="userSpaceOnUse"> +<stop stop-color="#1B3C3D"/> +<stop offset="1" stop-color="#164142"/> +</linearGradient> +</defs> +</svg> diff --git a/web/src/assets/svg/data-flow/knowledgegraph.svg b/web/src/assets/svg/data-flow/knowledgegraph.svg deleted file mode 100644 index b0feec924..000000000 --- a/web/src/assets/svg/data-flow/knowledgegraph.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756884949583" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11332" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M190.464 489.472h327.68v40.96h-327.68z" fill="#C7DCFE" p-id="11333"></path><path d="M482.34496 516.5056l111.26784-308.20352 38.54336 13.9264L520.86784 530.432z" fill="#C7DCFE" p-id="11334"></path><path d="M620.544 196.608m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" fill="#8FB8FC" p-id="11335"></path><path d="M182.272 509.952m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" fill="#C7DCFE" p-id="11336"></path><path d="M558.65344 520.9088l283.77088 163.84-20.48 35.47136-283.77088-163.84z" fill="#C7DCFE" p-id="11337"></path><path d="M841.728 686.08m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" fill="#B3CEFE" p-id="11338"></path><path d="M448.67584 803.77856l49.60256-323.91168 40.48896 6.20544-49.60256 323.91168z" fill="#C7DCFE" p-id="11339"></path><path d="M512 530.432m-143.36 0a143.36 143.36 0 1 0 286.72 0 143.36 143.36 0 1 0-286.72 0Z" fill="#4185FF" p-id="11340"></path><path d="M462.848 843.776m-102.4 0a102.4 102.4 0 1 0 204.8 0 102.4 102.4 0 1 0-204.8 0Z" fill="#8FB8FC" p-id="11341"></path></svg> \ No newline at end of file diff --git a/web/src/assets/svg/data-flow/processing-icon-bri.svg b/web/src/assets/svg/data-flow/processing-icon-bri.svg new file mode 100644 index 000000000..96c8e7699 --- /dev/null +++ b/web/src/assets/svg/data-flow/processing-icon-bri.svg @@ -0,0 +1,6 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21.8074 21.9283L30.4051 33.9033C30.9531 34.667 30.7785 35.7307 30.0148 36.2787C29.7258 36.4865 29.3785 36.5982 29.0223 36.5982H11.8273C10.8871 36.5982 10.125 35.8361 10.125 34.8963C10.125 34.54 10.2367 34.1928 10.4445 33.9033L19.0422 21.9283C19.5902 21.1646 20.6539 20.99 21.4176 21.5385C21.5676 21.6463 21.6996 21.7779 21.8074 21.9283Z" fill="#C6EFED"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.94336 3.39844H34.0285C35.9086 3.39844 37.4328 4.92266 37.4328 6.80273V27.2281C37.4328 29.1082 35.9086 30.6324 34.0285 30.6324H5.94336C4.06328 30.6324 2.53906 29.1082 2.53906 27.2281V6.80273C2.53906 4.92266 4.06328 3.39844 5.94336 3.39844Z" fill="#00BEB4" fill-opacity="0.2"/> +<path d="M34.0422 3.40625C35.9223 3.40625 37.4465 4.93047 37.4465 6.81055V27.2359C37.4465 29.116 35.9223 30.6402 34.0422 30.6402H5.95703C4.07695 30.6402 2.55273 29.116 2.55273 27.2359V6.81055C2.55273 4.93047 4.07695 3.40625 5.95703 3.40625H34.0422ZM34.0422 3.83164H5.95703C4.31211 3.83164 2.97852 5.16523 2.97852 6.81055V27.2359C2.97852 28.8812 4.31211 30.2148 5.95703 30.2148H34.0422C35.6871 30.2148 37.0207 28.8812 37.0207 27.2359V6.81055C37.0207 5.16523 35.6871 3.83164 34.0422 3.83164Z" fill="#00BEB4"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9785 11.6797C20.6836 11.6797 21.2551 12.2512 21.2551 12.9562V21.0414C21.2551 21.7465 20.6836 22.318 19.9785 22.318C19.2734 22.318 18.702 21.7465 18.702 21.0414V12.9562C18.702 12.2512 19.2734 11.6797 19.9785 11.6797ZM11.0422 11.6797C11.7473 11.6797 12.3187 12.2512 12.3187 12.9562V21.0414C12.3187 21.7465 11.7473 22.318 11.0422 22.318C10.3371 22.318 9.76562 21.7465 9.76562 21.0414V12.9562C9.76562 12.2512 10.3371 11.6797 11.0422 11.6797ZM28.9145 11.6797C29.6195 11.6797 30.191 12.2512 30.191 12.9562V21.0414C30.191 21.7465 29.6195 22.318 28.9145 22.318C28.2094 22.318 27.6379 21.7465 27.6379 21.0414V12.9562C27.6379 12.2512 28.2094 11.6797 28.9145 11.6797Z" fill="#00BEB4"/> +</svg> diff --git a/web/src/assets/svg/data-flow/processing-icon.svg b/web/src/assets/svg/data-flow/processing-icon.svg new file mode 100644 index 000000000..46acc8c1d --- /dev/null +++ b/web/src/assets/svg/data-flow/processing-icon.svg @@ -0,0 +1,6 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21.8074 21.9264L30.4051 33.9014C30.9531 34.665 30.7785 35.7287 30.0148 36.2767C29.7258 36.4846 29.3785 36.5963 29.0223 36.5963H11.8273C10.8871 36.5963 10.125 35.8342 10.125 34.8943C10.125 34.5381 10.2367 34.1908 10.4445 33.9014L19.0422 21.9264C19.5902 21.1627 20.6539 20.9881 21.4176 21.5365C21.5676 21.6443 21.6996 21.776 21.8074 21.9264Z" fill="#1C3C3D"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.94336 3.39844H34.0285C35.9086 3.39844 37.4328 4.92266 37.4328 6.80273V27.2281C37.4328 29.1082 35.9086 30.6324 34.0285 30.6324H5.94336C4.06328 30.6324 2.53906 29.1082 2.53906 27.2281V6.80273C2.53906 4.92266 4.06328 3.39844 5.94336 3.39844Z" fill="#00BEB4" fill-opacity="0.2"/> +<path d="M34.0422 3.4043C35.9223 3.4043 37.4465 4.92852 37.4465 6.80859V27.234C37.4465 29.1141 35.9223 30.6383 34.0422 30.6383H5.95703C4.07695 30.6383 2.55273 29.1141 2.55273 27.234V6.80859C2.55273 4.92852 4.07695 3.4043 5.95703 3.4043H34.0422ZM34.0422 3.82969H5.95703C4.31211 3.82969 2.97852 5.16328 2.97852 6.80859V27.234C2.97852 28.8793 4.31211 30.2129 5.95703 30.2129H34.0422C35.6871 30.2129 37.0207 28.8793 37.0207 27.234V6.80859C37.0207 5.16328 35.6871 3.82969 34.0422 3.82969Z" fill="#1B3B3C"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9785 11.6797C20.6836 11.6797 21.2551 12.2512 21.2551 12.9562V21.0414C21.2551 21.7465 20.6836 22.318 19.9785 22.318C19.2734 22.318 18.702 21.7465 18.702 21.0414V12.9562C18.702 12.2512 19.2734 11.6797 19.9785 11.6797ZM11.0422 11.6797C11.7473 11.6797 12.3187 12.2512 12.3187 12.9562V21.0414C12.3187 21.7465 11.7473 22.318 11.0422 22.318C10.3371 22.318 9.76562 21.7465 9.76562 21.0414V12.9562C9.76562 12.2512 10.3371 11.6797 11.0422 11.6797ZM28.9145 11.6797C29.6195 11.6797 30.191 12.2512 30.191 12.9562V21.0414C30.191 21.7465 29.6195 22.318 28.9145 22.318C28.2094 22.318 27.6379 21.7465 27.6379 21.0414V12.9562C27.6379 12.2512 28.2094 11.6797 28.9145 11.6797Z" fill="#00BEB4"/> +</svg> diff --git a/web/src/assets/svg/data-flow/raptor.svg b/web/src/assets/svg/data-flow/raptor.svg deleted file mode 100644 index 8f83bffc2..000000000 --- a/web/src/assets/svg/data-flow/raptor.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756884908677" class="icon" viewBox="0 0 1039 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11186" xmlns:xlink="http://www.w3.org/1999/xlink" width="202.9296875" height="200"><path d="M653.90935344 53.93964406c1.26171656 0.69905906 2.86687313 2.30421562 3.56593219 3.56593219 0.69905906 1.26171656 1.28607375 1.73425125 1.30556062 1.04980688 0.04384313-1.61246438-4.30883531-5.96514281-5.92129969-5.92129875-0.68444437 0.01948594-0.21190969 0.60650063 1.04980688 1.30555968m-209.72749781 23.39290688c-1.49311219 0.99622031-3.5001675 5.02494469-2.50638282 5.02494469 0.95724844 0 5.0005875-4.12615406 5.0005875-5.10288844 0-0.85251094-1.15697906-0.81597469-2.49420468 0.07794375m-30.74642438 76.49217656c-1.12044375 0.78431062-2.26524375 2.30178-2.54535562 3.37350844-0.6454725 2.46741094-0.55047844 2.45036063 2.60381343-0.47009906 3.03250406-2.81085094 2.98866094-5.03712375-0.05845781-2.90340938m29.83789125 122.86878469c-1.40786156 1.40786156-2.28472969 2.8327725-1.94859656 3.16890562 0.33613313 0.33613313 1.76348062-0.54073594 3.17134125-1.94859656 1.40786156-1.40786156 2.28472969-2.83520906 1.9485975-3.16890562-0.33613313-0.33613313-1.76348062 0.54073594-3.17134219 1.94859656m181.00030594 42.51838687c0.02679281 1.32261 1.40299031 2.82059437 3.69989812 4.03116 1.88039625 0.99134906 2.14832812 0.97429875 1.53452063-0.09499406-2.01436219-3.51965344-5.27582625-5.97244969-5.23441875-3.93616594m-174.16560188 79.07650313c-1.01570625 0.98647687-1.39324687 1.79514469-0.83789625 1.79514469 1.10826469 0 3.66823406-2.72316469 3.0617325-3.25902844-0.20703844-0.18268125-1.20813 0.47497031-2.22383625 1.46388375m264.44653781 19.57852875c-0.11935125 1.80975938 2.90097375 5.25390469 4.54753782 5.18813906 1.2812025-0.05115094 1.18864406-0.27523969-0.43112719-1.05954937-1.13262187-0.54804281-2.50881844-1.92423938-3.05686125-3.05442563-0.54804281-1.13262187-1.02544875-1.6149-1.05954937-1.07416406m129.50132156 9.36787969c1.31043187 0.66252281 2.92289531 2.27498719 3.58541906 3.58541906 0.88661156 1.75130156 1.20813 1.93641844 1.22030906 0.69905906 0.01705031-2.16537844-3.33940781-5.52183656-5.50722281-5.50478719-1.23492375 0.00974344-1.04980688 0.33126187 0.70149469 1.22030907m-529.46060344 15.81773625c-1.49311219 0.99622031-3.5001675 5.02494469-2.50638281 5.02494468 0.95724844 0 5.0005875-4.12371844 5.0005875-5.10045281 0-0.85251094-1.15697906-0.81597469-2.49420469 0.07550813m-207.76672125 92.45849343c-1.52477719 2.84738719-0.75021 3.03006844 1.28850937 0.30446813 0.947505-1.26902344 1.28850937-2.30665219 0.75264563-2.30665219-0.53342813 0-1.45170469 0.90122625-2.041155 2.00218406m213.38355281 27.99646688c-3.28338563 2.29447313-3.94590937 3.59759719-1.83168187 3.59759812 1.24466625 0 3.43683844-2.05576969 4.85931375-4.55971781 0.94263375-1.6563075 0.52855688-1.52234156-3.02763188 0.96211969m101.26615125 13.87401094a13.676715 13.676715 0 0 0 4.32101344 0c1.18864406-0.22896 0.21678187-0.41407688-2.16050719-0.41407688-2.3748525 0-3.34671562 0.18511687-2.16050625 0.41407688m286.65323625 32.20056562c-4.5110025 5.14673156-4.90072125 6.07718719-1.55887781 3.73399969 3.13724156-2.19947906 5.81899781-5.89450594 4.27717031-5.89450594-0.45304875 0-1.67822906 0.97186312-2.7182925 2.16050625m288.73579875 0.06089438c-3.49286063 3.71694844-1.85603906 5.22711188 1.75130156 1.61733562 1.97782594-1.97539031 2.52586875-3.83873625 1.12775063-3.83873625-0.43599844 0-1.73181562 1.00109156-2.87905219 2.22140063m-532.60271625 38.333775c-3.3223575 3.69259125-3.66823406 4.92020719-0.86956125 3.08365406 2.65496344-1.73668687 5.08827375-5.24172562 3.64144031-5.24172563-0.45548437 0-1.70258625 0.97186312-2.77187906 2.15807157m388.45038 14.52191906c0 1.47119062 1.51259813 2.86200187 4.12858969 3.79732781 1.8536025 0.66252281 1.54669875-0.23383125-1.16185031-3.39786562-1.761045-2.05576969-2.96673937-2.218965-2.96673938-0.39946219m-198.49383562 54.40969969c0.90366188 0.52612125 2.30908781 1.90475344 3.12019125 3.06416906 0.81110344 1.15697906 1.47362625 1.57592812 1.47362625 0.93045562 0-1.54669875-3.51234656-5.02007344-5.03955844-4.98353718-0.68200875 0.01705031-0.489585 0.44330625 0.44574094 0.9889125m36.27069843 22.77909937c0 0.97429875 4.13346188 5.0005875 5.1345525 5.0005875 0.37997625 0 0.23383125-0.85494656-0.32395406-1.89744656-1.10095687-2.05820531-4.81059844-4.45010812-4.81059844-3.10314094m17.96606438 16.38039375c3.05199 2.94238125 3.74861344 3.04711875 2.29934437 0.34100438-0.55291406-1.03519219-1.89988219-2.11666313-2.98866093-2.40164532-1.70745844-0.44574188-1.61002875-0.15832313 0.68931656 2.06064094m-403.8467325 20.11195688c0 1.29825281 2.29447313 3.719385 4.13102531 4.35998625 1.20325875 0.41894813 1.52477719 0.24113906 1.0522425-0.58701469-1.97295469-3.47093812-5.18326781-5.80681875-5.18326781-3.77297156m-213.99248906 15.08944875c-0.11935125 1.80488812 2.72560031 5.00545875 4.94456531 5.56567968 2.11179188 0.5309925 2.12153531 0.50176406 0.32638969-0.84033187-1.02544875-0.76482469-2.1458925-1.39324687-2.49176813-1.39324688-0.34831125 0-1.09852125-0.97186312-1.67335781-2.15807156-0.5724-1.18864406-1.06929281-1.71720094-1.10582906-1.17402937m373.336575 136.54549968c0 1.53208406 3.03737531 5.01276563 4.3746 5.01276657 1.97782594 0 1.72938-0.73072406-1.15454344-3.40517344-2.72560031-2.52586875-3.22005656-2.77187906-3.22005656-1.60759313m213.26420062 47.55063938c-3.49042406 3.71694844-1.85603906 5.22711188 1.75373719 1.61733562 1.97782594-1.97782594 2.52343312-3.83873625 1.12775062-3.83873625-0.43599844 0-1.73181562 0.99865594-2.88148781 2.22140063" fill="#bfbfbf" p-id="11187"></path><path d="M508.11045312 34.32214344c0 1.67822906-0.78918188 1.90718906-7.43876906 2.15807156l-7.43876906 0.28011094-0.29959688 3.60003281-0.29716125 3.60003281h-6.46934249c-7.15865812 0-8.77355812 0.91827656-8.77355813 4.99084406 0 2.49664031-0.27280313 2.68906406-3.83873625 2.68906407-3.07878375 0-3.84117187 0.34831125-3.84117187 1.75130156 0 2.59163437-3.81437906 5.677725-7.96489032 6.44498437-4.18461187 0.77213156-6.43280625 2.6354775-6.43280531 5.33428407 0 1.32017437-0.86225438 2.04846281-2.91315281 2.45766843-3.19082719 0.63816563-4.17974062 1.97051906-5.23929 7.04904938-0.80379656 3.86309344-2.905845 6.58138688-6.00655031 7.76515969-1.67335781 0.64060125-2.15807156 1.58567062-2.15807157 4.20896906 0 3.20300625-0.17293781 3.38325188-3.2395425 3.38325187-3.45145219 0-4.44036563 1.93641844-4.44036562 8.69074219 0 2.47228219-2.92289531 5.88232687-5.88719813 6.87124031-0.54560719 0.18268125-1.2812025 3.55375406-1.63195031 7.49479125-0.62842219 7.03199906-0.69418781 7.17570844-3.51965344 7.65798657-3.30774375 0.56752875-3.96539531 4.27960594-4.20166218 23.72904l-0.11935125 9.80631468-3.35889469 3.35889469-3.35889375 3.36132938 0.09499406 9.86964375c0.11691562 11.94733594 0.69662344 14.31975281 3.88014375 15.8542725 2.43574594 1.17402937 2.45766844 1.29094594 2.99109656 15.89811656 0.5724 15.67646344 1.87308844 21.12035625 5.31479813 22.21156969 1.58567062 0.50419969 2.00949094 1.81706719 2.58919875 8.01117 0.71367375 7.65555094 1.37376094 8.81009438 5.04686625 8.83445156 1.70989406 0.01217906 2.16294281 0.56509312 2.17512094 2.65496344 0.02679281 4.79354906 2.49907594 10.25692781 5.06148093 11.1873825 1.77809438 0.6454725 2.6038125 1.87796063 3.21762094 4.79842031 0.74046656 3.5318325 1.14480094 3.96295875 3.94590844 4.23332719l3.11775562 0.29959687v5.53157906c0 5.71182469-1.65874312 8.48126812-5.99924343 10.01335313-1.16915813 0.41164125-1.67822906 1.61977125-1.67822907 3.98244562 0 3.23223562-0.1558875 3.39055875-3.34915125 3.39055875-3.1567275 0-3.38325188 0.21678187-3.95321625 3.78514969-0.54317156 3.38812312-0.92558344 3.81925031-3.61221093 4.07987438-2.71585688 0.26549625-3.03250406 0.63816563-3.3028725 3.88014375-0.25818937 3.11288344-0.69418781 3.69015563-3.29312907 4.36242187-3.24684938 0.84276844-3.99218812 2.32613812-5.03225156 10.04988938-0.59675813 4.43549438-0.70149469 4.55728125-3.91424437 4.55728125-3.20300625 0-3.30043594 0.11448-3.30043594 3.78271406 0 3.67797656-0.09986531 3.79002094-3.60003281 4.07987437-3.40517344 0.28254656-3.6146475 0.49202062-3.89719407 3.89719407-0.2703675 3.26389969-0.55778625 3.60003281-3.08609062 3.60003375-3.26877187 0-4.77649875 2.19947906-4.77649875 6.96867 0 3.30287156-2.47228219 6.49126406-5.99924344 7.73593031-1.15454344 0.40677-1.67822906 1.6149-1.67822906 3.86065781 0 3.06173344-0.23870344 3.30530813-3.78271406 3.87040031-3.54888187 0.56752875-3.79976437 0.82571813-4.07987438 4.20409782-0.26549625 3.20057063-0.61380844 3.63169781-3.17377781 3.92398781-3.33210094 0.37754063-4.28691375 1.52721281-4.30883532 5.17352531-0.00974344 2.01923344-0.58945031 2.76700781-2.51612531 3.249285-2.90828063 0.73072406-5.17596094 5.26851938-5.17596094 10.35922875 0 3.1567275-0.18998813 3.34915125-3.60003281 3.62926219-3.40517344 0.28254656-3.61708313 0.49445625-3.89719406 3.89962969-0.2703675 3.26389969-0.55778625 3.59759719-3.08609063 3.59759718-2.95699594 0-4.77649875 2.16050719-4.77649875 5.67041719 0 1.40299031-0.88174031 2.17999313-3.12019125 2.75482969-3.03493969 0.77700281-3.14211281 0.98404125-3.99705937 7.65798562l-0.87686813 6.85906219-3.44170968 0.28741781c-3.22492781 0.2703675-3.46119563 0.51637781-3.74130657 3.88745063-0.28011094 3.40273781-0.47740594 3.60003281-3.58785375 3.60003375-3.09583312 0-3.32479406 0.22652438-3.90450187 3.84117187-0.489585 3.07147594-1.0205775 3.84117187-2.65009219 3.85335-3.25172156 0.02435719-4.10179688 1.12044375-4.77649781 6.16974563-0.62598656 4.67906812-3.53913937 9.17302031-5.94322125 9.17302031-0.63329437 0-1.4273475 1.72938-1.76591625 3.84117188-0.57970781 3.62682656-0.79648875 3.83873625-3.96052312 3.83873625-3.25902844 0-3.34915125 0.10230094-3.34915125 3.78271406 0 3.67554094-0.09986531 3.79002094-3.60003282 4.07987531-3.38081625 0.27767531-3.61708313 0.50907094-3.88988718 3.78758531-0.25088156 3.00571125-0.68444437 3.56836875-3.13724063 4.05795282-3.33940781 0.66739406-4.73021906 3.52939594-4.73022 9.73324218 0 4.16025469-0.03410062 4.20166219-3.60003281 4.49882344-3.40517344 0.28011094-3.61708313 0.49202062-3.89719406 3.89719406-0.25331719 3.05686125-0.64060125 3.60003281-2.562405 3.60003282-4.11641156 0-5.30018344 0.99134906-5.30018344 4.44523687 0 2.88635906-0.35074781 3.35158687-2.91315281 3.86309344-3.40273781 0.68200875-4.76675531 2.76944344-4.76675532 7.29749625 0 3.52696031-3.18352031 7.43146219-6.06013687 7.43146219-1.24466625 0-1.61733562 0.8744325-1.61733563 3.7802775 0 3.67797656-0.09986531 3.79002094-3.60003281 4.07987531-3.40517344 0.28254656-3.61708313 0.49445625-3.89719406 3.89719406-0.27280313 3.29069344-0.54560719 3.60003281-3.16647 3.60003281-2.31883031 0-3.12749812 0.59919375-4.22358469 3.12019125-1.15210781 2.64765656-1.36158187 2.78892937-1.39324594 0.93532688-0.05358656-3.06660469-3.3540225-4.05551719-13.54031437-4.05551813-8.2498725 0-8.57626219-0.082815-8.59087688-2.16050625-0.03410062-5.22467531 1.61246438-5.03955938-44.437755-5.03955937-45.82856625 0-45.77741531-0.00487125-45.77741531 4.91777156 0 2.21409375-0.23383125 2.28229406-7.67990812 2.28229406h-7.67990813l-3.83873625 3.83873625c-3.09583312 3.09583312-4.53779531 3.84604312-7.43876906 3.86552907-3.14211281 0.02435719-3.89475844 0.50663531-5.92617 3.79732875-2.20678594 3.57811125-2.59894125 3.78514969-7.6774725 4.05308156l-5.35377094 0.28498312-0.29959594 3.59759719c-0.28985344 3.5001675-0.40189781 3.60003281-4.07987531 3.60003281-3.770535 0-3.78027844 0.00974344-3.78027844 3.84117188v3.83873625h-7.67990812v7.67990812h-3.83873625c-3.82899281 0-3.84117187 0.00974344-3.84117188 3.7802775 0 3.67797656-0.09742969 3.79002094-3.59759718 4.07987532-3.40517344 0.28254656-3.61708313 0.49445625-3.89719407 3.89719406-0.24601031 2.96673937-0.66739406 3.60246844-2.40164531 3.6146475-3.85578656 0.02679281-4.7911125 1.47119062-5.37812812 8.29858781-0.55047844 6.43280625-0.62111531 6.57164344-3.43927407 6.89316188-3.82412156 0.43356281-4.291785 1.40786156-4.30883437 9.03174656-0.01217906 5.8336125-0.20460281 6.479085-1.93398281 6.47908594-1.89501094 0-1.91936813 0.64060125-1.91936813 52.31495812s0.02435719 52.31495812 1.91936813 52.31495813c1.74399469 0 1.91936813 0.63816563 1.91936812 7.00520625 0 7.72618687 0.85251094 9.31185844 4.99327969 9.31185843 2.57945531 0 2.68662844 0.18998813 2.68662844 4.73752594 0 6.70317375 1.20569437 9.27775781 4.66445343 9.96951 2.53317656 0.50663531 2.93263875 1.03275656 3.19569938 4.21627688 0.28254656 3.43196625 0.49445625 3.65118375 3.8436075 3.92885906 3.33210094 0.27767531 3.56349656 0.50907094 3.84117187 3.83873531 0.27523969 3.33210094 0.50663531 3.56349656 3.83873625 3.84117188 3.32966531 0.27767531 3.56349656 0.50907094 3.83873625 3.83873625 0.27767531 3.34184344 0.50176406 3.56349656 3.89475844 3.8436075 3.38081625 0.28011094 3.60003281 0.49202062 3.60003281 3.49773187 0 3.37837969 1.21056562 4.36242187 5.35864219 4.36242188 1.9997475 0 2.32126594 0.45304875 2.32126594 3.2712075 0 3.19082719 0.11204438 3.28338563 4.76188406 3.92398687 3.29800031 0.45304875 4.92264281 1.16915813 5.29044094 2.32613719 0.61137188 1.92180375 7.9064325 5.83848375 10.87804312 5.83848469 1.22030906 0 2.4893325 1.00352719 3.25415625 2.57701875 1.45657594 3.00083906 3.8119425 3.77297063 14.30513813 4.68881156 6.93944156 0.604065 7.58491406 0.83789625 7.58491406 2.74508625 0 4.39408594-0.08037937 4.38677906 45.68485688 4.38677906 46.7297925 0 44.54492812 0.23383125 44.54492812-4.74726937 0-1.48824094 1.3956825-1.82193844 10.76843438-2.562405l10.76843437-0.85007532 0.86712563-3.03737531c0.81841031-2.87661656 1.17890156-3.07634719 6.7494525-3.70720594 6.72996656-0.76482469 8.28397312-1.68066469 8.28397312-4.89097875 0-1.80975938 0.50907094-2.28229406 2.46253969-2.28229406 3.999495 0 6.17461687-1.68310031 6.17461687-4.77406312 0-2.52830438 0.33613313-2.81815875 3.59272594-3.08609063 2.58432656-0.21434531 3.68528437-0.79405313 3.9264225-2.06551219 0.39946219-2.10935625 3.51478219-5.40492094 5.20031813-5.50235156 2.2603725-0.13153031 6.9589275-3.40517344 7.9064325-5.50722187 1.22761594-2.72316469 3.15429187-2.69637094 3.54644718 0.04871437 0.4165125 2.92776656 4.78624125 4.864185 12.08373657 5.35133531 5.13698906 0.34344 6.29640375 0.77456719 7.90886812 2.93020219 1.04006344 1.38837562 2.3431875 3.06660469 2.89853813 3.72669188 0.61624406 0.73072406 3.427095 1.1983875 7.20980906 1.1983875 5.78733281 0 6.20140969 0.146145 6.21602437 2.16050718 0.02679281 4.07013187 1.43465438 4.94943656 8.56895532 5.35377 6.70560938 0.37754063 6.77381063 0.40920562 6.77381062 3.09826875 0 3.48555281 0.57970781 3.78514969 8.58844125 4.45741594 6.62279438 0.55534969 6.67150875 0.57970781 6.96136313 3.54401063 0.20947406 2.16781406 0.81597469 3.03493969 2.20922156 3.17134218 13.19930906 1.28850937 12.82663969 1.16185125 13.14328687 4.43549344l0.29472563 3.05686219 7.3851825 0.28011094 7.38274687 0.27767437 0.29472563 3.56106094 0.29716125 3.56106187 7.43876906 0.28011c7.41197625 0.28011094 7.43876906 0.28985344 7.43876906 2.9277675 0 4.03846688 1.62951469 4.95187219 8.83201594 4.95187219 6.26961094 0 6.52780031 0.09012281 6.52780031 2.33100938 0 3.97270219 2.36023781 5.34646312 9.187635 5.34646312 5.75323313 0 6.16974563 0.146145 6.18435938 2.16050625 0.02679281 3.96295875 1.45901156 4.92020719 7.99655531 5.33672063l6.22333125 0.39459093 0.32151844 24.70577438c0.34587562 26.47656187 0.60893625 27.87711656 5.23929094 27.87711562 2.20191469 0 2.28229406 0.25575375 2.28229406 7.229295 0 8.52267656 1.761045 13.511085 5.23198312 14.83125938 2.0630775 0.78431062 2.447925 1.53451969 2.447925 4.79354906 0 2.60868469 0.42138375 3.86309344 1.29825282 3.86309344 2.93507437 0 6.37921969 4.59625312 6.37921875 8.51293312 0 5.69964656 0.73072406 6.8444475 4.3648575 6.84444657 3.00327563 0 3.21762094 0.2192175 3.49773187 3.60003375 0.28254656 3.40273781 0.49202062 3.6146475 3.89719406 3.89719406 3.5001675 0.28985344 3.60003281 0.40189781 3.60003282 4.07987437 0 3.77297063 0.00974344 3.78027844 3.83873625 3.78027844h3.84117187v3.84117188c0 3.82899281 0.00974344 3.83873625 3.78027844 3.83873625 3.67797656 0 3.79002094 0.09986531 4.07987437 3.60003281 0.28254656 3.40760906 0.49932844 3.62439 4.09936125 4.07987531 3.07878375 0.38971969 3.90693656 0.93532688 4.35755063 2.87905219 0.63816563 2.740215 5.00545875 4.65958219 10.80009844 4.74483375 3.07147594 0.04627875 3.60003281 0.37023375 3.61221187 2.21409281 0.031665 4.48664438 1.21787344 5.02007344 11.94977156 5.36838469 9.19981406 0.29716125 10.11321844 0.49202062 10.11321844 2.15807156 0 1.78053094 1.4273475 1.83411656 49.43590594 1.83411656 47.61396844 0 49.43590594-0.06576469 49.43590687-1.79270906 0-1.59784969 1.03275656-1.84142437 9.56030344-2.25062906 9.9792525-0.48227813 12.19090969-1.33478906 12.69754594-4.90559344 0.23870344-1.66361437 1.04493469-2.15319938 3.94590844-2.39190281 2.70611437-0.22165313 3.85335094-0.84276844 4.44767343-2.39920969 1.38350344-3.6146475 4.16269031-5.31967031 9.19007063-5.6363175 4.65958219-0.29472562 4.81790625-0.40189781 5.68259531-3.90206531 0.79648875-3.2298 1.20569437-3.60003281 3.97513875-3.60003281 3.26146406 0 4.25524875-1.2495375 4.25524875-5.35864219 0-1.99731188 0.45061313-2.32126594 3.25659281-2.32126594 3.43927406 0 4.42087969-1.18864406 4.42087875-5.35620562 0-2.01192656 0.4481775-2.32126594 3.34915125-2.32126688 3.16403437 0 3.38081625-0.21190969 3.96295969-3.84117187 0.50419969-3.15916313 0.99622031-3.83873625 2.77187906-3.83873625 3.15429187 0 4.04577469-1.64412844 4.81303407-8.87829469l0.68444531-6.479085h3.249285c2.93263875 0 3.24684938-0.26549625 3.24685031-2.74752188 0-8.64446344 1.07172844-11.58440906 4.4476725-12.22013906l3.23223563-0.604065v-21.61968375l3.83873625-0.61380844 3.84117187-0.61380843v-35.2915275c0-38.3751825-0.15832313-39.55895531-5.30262-39.55895532-2.2920375 0-2.31639469-0.10230094-2.58676219-10.79766281l-0.27036843-10.80009937-3.05442563-0.89879063c-3.3223575-0.97917-4.10179688-2.61842719-4.14564-8.70048562-0.03410062-4.72291219-0.74046656-5.906685-4.01898187-6.72996657-2.0630775-0.51637781-2.68419281-1.37132531-3.19326282-4.39408593-0.5943225-3.50747437-0.85007531-3.75348469-3.90937312-3.75348563-3.08121937 0-3.27607875-0.19242375-3.27607875-3.24441375 0-2.48202562-0.896355-4.16756156-3.81681469-7.17814406-2.10204937-2.16294281-4.44280125-5.4268425-5.20518937-7.25365313-1.11557156-2.66957813-2.20191469-3.55375406-5.5413225-4.50613031-4.78380562-1.36645406-7.3851825-3.30287156-8.56164844-6.3743475-0.64060125-1.67579344-1.58567062-2.16050719-4.20896906-2.16050719-3.06416906 0-3.38325188-0.25331719-3.38325188-2.68662843 0-3.48311719-1.59054187-4.99327969-5.2612125-4.99327969-2.35536656 0-3.16159875-0.54560719-3.96783-2.67932063-1.09852125-2.90828063-6.05770125-4.99815094-11.86208437-4.99815187-2.12884219 0-3.67554094-0.718545-4.90072219-2.27985844-3.23710687-4.11153937-7.52889188-5.40004969-18.00747188-5.40004969h-9.63337687l-0.29959594-3.12019031c-0.38971969-4.01898094-2.72316469-4.33562813-33.03846281-4.46472281-25.77263156-0.10960875-33.89828062 0.50176406-34.70938406 2.61842719-1.49311219 3.89475844-3.91424438 4.96648688-11.2190475 4.96648593-11.0412375 0-16.80908531 1.53208406-17.93683594 4.76431969-0.82815375 2.37241688-1.50529125 2.71098563-6.47421375 3.23223563-7.50209812 0.78431062-8.75894344 1.39081125-9.38980125 4.55241-0.46035562 2.29690875-1.06929281 2.7499575-4.03116094 2.99840343-3.27851437 0.27280313-3.50747437 0.50907094-3.78758531 3.88988719-0.27767531 3.34428-0.51637781 3.60003281-3.36132938 3.60490406-6.25743187 0.01217906-10.28372063 1.79758031-10.89022218 4.82764969-0.45304875 2.26524375-1.19107969 2.92045969-3.83142844 3.41735156-3.40030219 0.63816563-5.13698906 2.69393531-5.13698906 6.08692969 0 1.44683344-0.61624406 1.98269719-2.28229407 1.98269813-2.93020313 0-4.10179688 1.47606187-4.94943656 6.23794593-1.09608562 6.14295188-3.60734062 8.15974969-10.15706156 8.15974969h-5.53401563l-0.29716125-3.60003281-0.29716125-3.60003281-7.3851825-0.27767532-7.38274687-0.28011093-0.29472563-3.56106094-0.29716125-3.55862531-7.43876906-0.28254657-7.43876906-0.28011093v-3.21518532c0-3.90206531-1.010835-4.38190781-9.1973775-4.38190687h-6.16243781v-3.24928594c0-3.8022-1.3128675-4.39652156-9.77708625-4.41844312-5.43902156-0.01217906-5.4585075-0.02192156-5.76054-3.09339844-0.34587562-3.55862531-2.12640656-4.35024281-10.13757563-4.50125906-4.91533594-0.09499406-5.03955938-0.16563094-5.03955937-2.92045969 0-3.58054688-0.28498219-3.7291275-8.52024-4.49638781-6.24768938-0.57970781-6.83957531-0.82084687-6.83957532-2.77918594 0-3.15916313-1.76835188-4.13102531-8.82470906-4.8544425-5.94322125-0.60893625-6.37678406-0.81597469-6.87124031-3.29069344-0.56996437-2.85713062-1.54426313-3.28338563-10.05476063-4.39408594-4.09692562-0.53342813-5.27826188-1.09852125-5.94078468-2.83764468-0.91584094-2.41138875-4.48177313-3.80707125-12.22257563-4.78380563l-5.03955844-0.63573v-3.63900469c0-2.83033688 0.42625594-3.74861344 1.91936813-4.13833312 1.77322312-0.46522781 1.91936813-1.31043187 1.91936812-11.03393063v-10.53216656l20.58205594-0.39702656c21.99966-0.42625594 24.53283656-1.0205775 24.53283656-5.74592531 0-1.65874312 1.19107969-1.77322312 18.9574125-1.82680969 24.48168563-0.07063688 26.5423275-0.4165125 26.93935407-4.51343813l0.29959687-3.11044781h43.21501031l3.44658094-3.83873625 3.44658094-3.83873625 19.76121-0.05602219c25.07113594-0.07063688 27.1463925-0.39946219 28.35939375-4.50856687l0.91827656-3.11531906h21.32008688c21.67327031 0 24.35746312-0.25818937 24.35746312-2.33588063 0-4.55971688 3.49042406-5.3440275 23.79967594-5.3440275 21.5319975 0 26.56181344-0.97917 27.6676425-5.385435l0.57483562-2.2920375h21.90466688c23.77288406 0 24.92255625-0.24844594 24.92255531-5.34889875v-2.33100844h43.83369l3.08609063-3.84117187 3.08609062-3.83873625h21.49058906c23.75096156 0 25.92364781-0.38241188 26.88089625-4.74239813l0.6454725-2.93751h22.88383688v2.32126594c0 4.16999719 0.98160562 5.35864219 4.42331531 5.35864219h3.25659281v6.17218031c0 6.79816781 1.37376094 9.187635 5.29044 9.187635 1.93398281 0 2.31639469 0.53586375 2.56971282 3.59759719 0.28254656 3.39299437 0.50176406 3.61708313 3.8436075 3.89475844 3.33210094 0.27767531 3.56349656 0.50907094 3.83873625 3.84117187 0.27767531 3.33940781 0.50176406 3.56106094 3.89475843 3.84117188 3.38081625 0.28011094 3.60003281 0.49445625 3.60003282 3.49773187 0 3.37837969 1.21056562 4.3648575 5.35864218 4.3648575 1.76348062 0 2.32370156 0.51881437 2.33588063 2.15807156 0.02435719 3.80463562 1.34940375 4.6254825 8.41550344 5.21736844 6.77381063 0.56752875 6.82739719 0.59675813 7.11725062 3.51965344 0.35074781 3.56349656 2.16781406 4.23089156 11.91567 4.36729219 7.07097187 0.09742969 7.572735 0.36779812 10.93650094 5.85553406 0.75264562 1.22274469 5.60952375 1.47849844 32.35645406 1.70258719 35.01628875 0.29229 39.11321344-0.18998813 41.3638425-4.86174938 1.14236531-2.3748525 1.72938-2.60137688 6.72022406-2.61355594 8.47152562-0.02192156 9.78682875-0.61380844 9.78682875-4.41844406v-3.249285h5.82143344c7.29749625 0 9.53594625-1.12044375 9.53594719-4.77406312 0-2.52830438 0.3336975-2.81572312 3.60003281-3.08609063 3.39299437-0.28011094 3.61708313-0.50176406 3.89475844-3.8436075 0.27523969-3.32966531 0.50907094-3.56106094 3.83873625-3.83873625 3.33210094-0.27767531 3.56349656-0.50907094 3.83873531-3.83873625 0.27767531-3.33210094 0.50907094-3.56349656 3.84117188-3.84117187 3.32966531-0.27523969 3.56106094-0.50907094 3.83873625-3.83873625 0.27767531-3.33210094 0.50907094-3.56349656 3.84117187-3.84117188l3.54401156-0.29472469 0.29716032-5.26608375c0.31421156-5.55593719 1.89257531-8.24013 5.70208218-9.69670593 1.97539031-0.75508125 2.16050719-1.44439781 2.16050719-8.05014094 0-6.5838225 0.16806656-7.22198813 1.91936813-7.22198813 1.89257531 0 1.91936813-0.64060125 1.91936812-45.1148925 0-44.47672688-0.02679281-45.11732813-1.91936812-45.11732812-1.66361437 0-1.93398281-0.66983062-2.01679782-5.03955938-0.15101625-8.01116906-0.94019812-9.7917-4.50125906-10.14001125-2.87905219-0.28011094-3.08121937-0.54073594-3.08121937-3.97513781 0-3.88745062-1.05467812-4.84226344-5.35864219-4.84226437-1.99244062 0-2.32126594-0.45304875-2.32126594-3.21518438 0-5.37812812-1.76591625-9.02200406-4.9055925-10.11565406a6.14051625 6.14051625 0 0 1-3.83873625-3.84117188c-1.125315-3.22492781-4.75457719-4.90559344-10.59549656-4.90559343-3.67310531 0-3.6974625-0.02435719-3.6974625-3.83873625v-3.84117188h-3.83873625c-3.427095 0-3.84117187-0.25088156-3.84117188-2.33100937 0-3.95078062-2.36511-5.34646312-9.06584812-5.34646313-5.96270719 0-6.06013688-0.04384313-6.63010125-2.89610156-0.94019812-4.698555-2.22383625-4.8227775-48.84889125-4.76431969-23.76314062 0.031665-43.26129-0.06820125-43.32461906-0.22165312-1.03519219-2.44061812-2.58189094-4.29422063-4.20653344-5.03468813-1.18133719-0.54073594-2.24819344-2.18242875-2.58676313-3.98244469-0.63085875-3.36863719-1.92180375-4.78867688-4.84469906-5.32454156-1.18620844-0.2192175-2.04602719-1.26658781-2.25063-2.74508625-0.45061313-3.23710687-1.6879725-4.55241-4.95917906-5.270955-2.23845094-0.49202062-2.81085094-1.17890156-3.05199-3.66092625-0.24601031-2.54291906-0.85981875-3.23710687-3.656055-4.1358975-2.66470687-0.85494656-3.427095-1.64900062-3.68528438-3.83629969-0.36536156-3.07634719-1.56374906-4.16756156-4.60599656-4.18948406-1.57105594-0.00974344-2.19704344-0.781875-2.66714156-3.28095-0.61380844-3.27851437-2.01192656-4.41113625-5.43902156-4.41113625-1.37619656 0-1.84142437-0.67957313-1.84142438-2.68906406 0-3.41004469-1.58079938-4.99084406-4.99084406-4.99084406-2.30421562 0-2.68662844-0.3848475-2.68662844-2.68662844 0-3.41248031-1.58079938-4.99327969-4.99327969-4.99327969-2.41382438 0-2.68662844-0.32882531-2.6866275-3.25659281 0-3.43927406-1.18864406-4.42087969-5.35864218-4.42087969-1.99731188 0-2.32126594-0.45304875-2.32126594-3.25902844 0-3.43927406-1.18864406-4.42087969-5.35620656-4.42087968-2.01192656 0-2.32126594-0.4481775-2.32126594-3.34915125 0-3.16403437-0.21190969-3.38081625-3.84117188-3.96052313-3.51965344-0.56509312-3.84117187-0.86469-3.86552906-3.61221187-0.01948594-2.42113219-0.72341625-3.427095-3.66092719-5.23685438-2.83764469-1.74886594-3.89232281-3.19326375-4.79598468-6.57164344-0.91827656-3.43927406-1.75130156-4.55241-4.04577375-5.41953562-1.95834-0.74046656-3.1250625-1.9997475-3.61951969-3.90937219-0.71367375-2.75239313-1.78783781-3.74130656-5.21006063-4.79842031-1.00352719-0.30933937-1.67822906-1.468755-1.67822906-2.87905219a5.3123625 5.3123625 0 0 0-5.34889969-5.37812812c-1.91206125 0-2.33100937-0.48227813-2.33100843-2.68906407 0-3.39299437-1.57836375-4.99084406-4.93238625-4.99084406-2.34805969 0-2.66227031-0.38728406-2.9277675-3.60003281-0.28254656-3.39299437-0.50176406-3.61708313-3.8436075-3.89475844-3.33210094-0.27523969-3.56349656-0.50907094-3.83873625-3.83873625-0.26549625-3.18108469-0.59919375-3.57567563-3.25902844-3.83386406-2.53074-0.24601031-3.08852625-0.80623219-3.83873625-3.83873625-0.76238812-3.08609063-1.28607375-3.59029031-3.99705938-3.85335094-2.55022594-0.24601031-3.12019125-0.71123813-3.12019125-2.55996937 0-4.1042325-0.99378469-5.29774781-4.42087875-5.29774782-2.80597969 0-3.25659281-0.32151844-3.25659281-2.32126593 0-4.14807562-0.98647687-5.35864219-4.3648575-5.35864219-3.00327563 0-3.21762094-0.2192175-3.49773187-3.59759719-0.26306062-3.19326375-0.62355094-3.64387687-3.17621344-3.98000906-2.82790125-0.375105-2.83520906-0.39215531-0.48227719-0.99378469 2.16781406-0.55534969 2.48689688-1.28850937 3.28338563-7.53863437 0.85981875-6.73240313 0.95968406-6.9272625 3.56593218-6.9272625 3.51721781 0 4.67176125-2.30665219 4.67176219-9.33621563 0-5.51452969 0.05845781-5.64118875 3.11288344-6.542415 3.833865-1.13018625 4.43062219-3.69502687 4.51100156-19.42020469 0.05358656-10.65882563 0.09012281-10.83663562 2.47228313-11.74273312 3.33940781-1.26902344 4.12858969-3.98488125 4.7911125-16.50218063 0.95237719-18.03913688-0.24357469-25.89928969-4.08961782-26.89551093l-3.12019125-0.80623125v-13.33083938c0-15.61800469-0.13153031-16.12707562-4.41113625-16.93087219l-3.26633531-0.61380843-0.00243656-6.85175438c-0.00243562-7.96976156-2.14102125-13.39904063-5.46581438-13.87157531-1.72938-0.24601031-2.16050719-1.0205775-2.39920968-4.31857781-0.28254656-3.86552906-0.42625594-4.03116-3.77540719-4.31127094-3.41735156-0.28498219-3.49042406-0.37754063-3.93373031-4.98597281-0.51637781-5.39030625-2.88392344-9.60171187-5.3951775-9.60171188-1.1983875 0-1.9997475-1.10095687-2.61842719-3.60003281-0.71123813-2.88392344-1.49067656-3.77784281-3.91911563-4.49151563-2.21652937-0.65521594-3.25415719-1.68553687-3.86309437-3.84117187-0.69418781-2.45523187-1.50529125-3.09583312-4.88123531-3.84847969-2.59407-0.57727219-4.25524875-1.54913437-4.62061032-2.69880656-0.31177594-0.98647687-1.61246438-2.27011594-2.88636-2.84982375-1.27389562-0.58214344-2.31639469-1.41760406-2.31639468-1.86090938 0-0.44330625-1.70015063-0.80623219-3.7802775-0.80623218-3.67797656 0-3.79002094-0.09986531-4.07987532-3.60003282l-0.29716125-3.60003375-7.3851825-0.28011-7.38274687-0.27767531-0.29716125-3.56106094-0.29472469-3.56106093-7.43877-0.28011094c-6.64958719-0.25088156-7.43876906-0.47984156-7.43876906-2.15807156 0-1.84386-1.01814187-1.88039625-52.79479969-1.88039625s-52.79480063 0.03653625-52.79480062 1.88039625m15.74466375 278.86371937c1.16915813 4.64983969 2.53804781 4.82521312 37.52997843 4.82521313 35.19653344 0 36.95514281-0.22896 36.95514282-4.83252v-2.36511h23.9994075v3.38325187c0 2.70611437 0.47009906 3.56106094 2.34805968 4.27717031 2.82303 1.07172844 5.0834025 4.29909188 5.91155625 8.44473282 0.66495844 3.32722969 1.81463062 4.62061031 4.57433157 5.14916718 1.05711375 0.20216719 2.24088656 1.72207219 2.87905218 3.70233469 0.82571813 2.55753375 1.82924531 3.57811125 4.20409782 4.27717031 2.51125406 0.74046656 3.12019125 1.42003969 3.12019125 3.48311719 0 2.96430281 1.41273281 4.39895813 4.79842031 4.86662063 1.92180375 0.26549625 2.49420375 0.99865594 2.87905125 3.69259125 0.41894813 2.92533094 0.86225438 3.39543 3.44414531 3.64874812 2.53074 0.24601031 3.08852625 0.80379656 3.83873625 3.83873625 0.76238812 3.083655 1.28607375 3.59029031 3.99706031 3.85091438 2.55022594 0.24601031 3.12019125 0.71367375 3.12019032 2.56240593 0 4.08474656 0.99865594 5.29531219 4.36242187 5.29531219 3.00571125 0 3.21762094 0.22165313 3.49773188 3.60003281 0.28011094 3.39299437 0.50176406 3.61708313 3.8436075 3.89475844 3.32966531 0.27767531 3.56349656 0.50907094 3.83873625 3.83873625 0.27767531 3.33210094 0.50907094 3.56349656 3.84117187 3.84117188 3.32966531 0.27523969 3.56106094 0.50907094 3.83873625 3.83873625 0.27767531 3.32966531 0.50907094 3.56349656 3.83873531 3.83873531 3.33210094 0.27767531 3.56349656 0.50907094 3.84117188 3.84117187 0.27767531 3.34184344 0.49932844 3.56106094 3.89232281 3.84117188 3.5001675 0.28985344 3.60003281 0.40433344 3.60003281 4.07987531 0 2.90828063 0.375105 3.78271406 1.61977125 3.78271406 2.20191469 0 6.06013688 3.77297063 6.06013688 5.92617094 0 0.96455531 0.40189781 1.75373719 0.89391937 1.75373719 0.49445625 0 1.16428687 0.84033281 1.49067657 1.87065281 0.32638969 1.02788531 1.46631937 2.1458925 2.53561125 2.48689688 1.06929281 0.33856875 2.73290719 2.46010406 3.70233468 4.72047656 1.17159375 2.73290719 2.52343312 4.29909188 4.04090344 4.67906906 1.51990594 0.38241188 2.58676219 1.62220688 3.2078775 3.72669094 0.69905906 2.37972375 1.69040813 3.37837969 4.04577469 4.07256844 2.30421562 0.6771375 3.12019125 1.47362625 3.12019125 3.03981093 0 3.94347281 1.03032094 5.15891062 4.36485656 5.15891063 3.00327563 0 3.21762094 0.22165313 3.49773188 3.60003281 0.27280313 3.30774375 0.56996437 3.63900469 3.656055 4.07987531 2.98622531 0.42625594 3.41248031 0.85251094 3.84117187 3.83873625 0.41894813 2.94968906 0.85007531 3.39543 3.52452563 3.656055 2.72316469 0.26306062 3.11044781 0.69175219 3.68284781 4.07987532 0.59919375 3.541575 0.85007531 3.78271406 3.91424437 3.78271406 2.82546563 0 3.27607875 0.32151844 3.27607875 2.32126594 0 4.14807562 0.98404125 5.35864219 4.3648575 5.35864125 3.00327563 0 3.21518531 0.2192175 3.49529532 3.60003375 0.28011094 3.3856875 0.50907094 3.61708313 3.8022 3.89232187 4.09448906 0.34100438 4.406265 1.57592812 3.86309437 15.30622969-0.29229 7.38761812-0.45304875 7.91861156-2.39921062 7.93322625-3.99462375 0.02922938-4.87879969 1.46631937-5.2806975 8.56895531l-0.38241282 6.77380969-18.37039781 0.05602219c-20.41398937 0.05845781-24.66193125 0.84763969-25.5729 4.74483375l-0.56022187 2.39921062-22.51603875 0.54317156c-12.38333438 0.29716125-22.603725 0.62111531-22.71089813 0.718545-0.10960875 0.09742969-0.48227813 1.69040813-0.82815375 3.53913938l-0.63085875 3.35889375-21.7292925 0.00487125c-24.65462344 0.00730688-28.28145 0.59188594-29.75507625 4.81547062l-0.99622031 2.85956625H609.02829219c-23.92390031 0-23.85569906-0.01217906-24.68385282 4.39408594l-0.60893625 3.24928594-22.21400625 0.25818937-22.21400625 0.25575282-0.29472468 3.541575-0.29472563 3.541575-24.75692531 0.29716125c-26.52284156 0.32151844-27.92826656 0.58214344-27.92826656 5.20031812v2.26280906l-22.78884188 0.25818844-22.78884187 0.25818938-0.49445719 2.87905218c-0.59919375 3.5001675-4.22114813 4.00923844-29.85737719 4.19922657-17.47404375 0.13153031-21.67570594 0.97673437-22.56962531 4.53535968-0.57483656 2.28960187-0.66495844 2.30421562-14.15412188 2.41138875-7.46556187 0.05845781-17.67621094 0.38241188-22.69384781 0.72098063l-9.11699812 0.61137281-0.3020325 3.12019031-0.29959688 3.12019125h-18.10003031c-9.95489531 0-18.78203906 0.260625-19.61750063 0.58214344-1.2495375 0.47984156-1.51747031-0.20216719-1.51747031-3.83873625v-4.42331531h7.67990813v-3.83873625c0-3.56593219 0.18998813-3.84117187 2.68662843-3.84117188 3.78027844 0 4.99084406-1.62951469 4.99084407-6.7177875 0-4.11153937 0.26306062-4.57676719 3.81437812-6.76650375 3.12993375-1.93154719 3.81925031-2.89853812 3.83873625-5.38787062 0.02435719-2.78892937 0.34100438-3.08852625 3.86552906-3.68528438 3.59759719-0.60650063 3.84117187-0.85251094 3.84117188-3.90450093 0-2.77431469 0.39215531-3.34428 2.63791406-3.83630063 4.78867688-1.04980688 5.03955938-1.28363812 5.03955844-4.7082975 0-2.25306563 0.4481775-3.38812312 1.33478906-3.38812312 3.20300625 0 5.47312219-3.31992187 6.04065094-8.82958032l0.5626575-5.46825 3.47093812-0.28985437c3.25659281-0.27280313 3.48798844-0.51394219 3.76809938-3.88988625 0.26549625-3.21274969 0.58214344-3.60003281 2.9277675-3.60003281 3.34671562 0 4.93482187-1.59541406 4.93482187-4.96405125 0-2.19460781 0.54317156-2.80110844 3.10557657-3.46606688 2.80841531-0.72828844 3.18352031-1.25928094 3.90206531-5.53888687 0.84763969-5.06148094 2.7182925-7.82848875 6.18923156-9.15597 1.67579344-0.64060125 2.16050719-1.58567062 2.16050625-4.20897 0-3.22492781 0.15832313-3.38325188 3.34915125-3.38325188 3.1567275 0 3.38325188-0.21678187 3.95321625-3.78758531 0.54073594-3.3856875 0.92314781-3.81925031 3.60977625-4.07987438 2.72072812-0.26306062 3.03493969-0.63573 3.30530813-3.89719406 0.23383125-2.8327725 0.87930469-3.90693656 3.020325-5.01276656 2.66957813-1.38106781 5.79951188-7.96245469 5.79951187-12.19090969 0-1.35914625 0.75021-1.74886594 3.35889375-1.74886594 3.2712075 0 3.36133031-0.09986531 3.36133031-3.78271406 0-3.67554094 0.09986531-3.79002094 3.60003282-4.07743875 3.38081625-0.28011094 3.61708313-0.50907094 3.88988718-3.79002187 0.25088156-3.00571125 0.68444437-3.56836875 3.134805-4.05795282 3.35158687-0.66983062 4.73265469-3.52939594 4.73265469-9.7917 0-3.86309344 0.21434531-4.25768438 2.32126688-4.25768437 4.12615406 0 5.35864219-0.98891344 5.35864125-4.29909281 0-2.91315281 0.26793187-3.17621344 3.83873625-3.74861344 3.60977625-0.57727219 3.84117187-0.81110344 3.84117187-3.885015 0-2.91315281 0.34100438-3.34671562 3.11775563-3.948345 3.70720594-0.80623219 4.52561625-1.84873125 4.54510218-5.79464063 0.01217906-2.15807156 0.66008719-3.30043594 2.39921063-4.23332625 1.31043187-0.70149469 2.6354775-2.07038438 2.94481687-3.03981187 0.30690375-0.97186312 0.96211969-1.76835188 1.45414032-1.76835188 0.49445625 0 0.896355-1.70015063 0.896355-3.7802775 0-3.67797656 0.09986531-3.79002094 3.60003281-4.07987531 3.40273781-0.28254656 3.6146475-0.49445625 3.89719406-3.89719406 0.2703675-3.26633531 0.55778625-3.60003281 3.08609063-3.60003281 3.06173344 0 4.77406313-2.17268531 4.77406218-6.05526563 0-1.82680969 0.62598656-2.46984656 2.82546563-2.90828062 3.05442562-0.61137188 4.83739219-4.61817469 4.84957125-10.89752907 0.00487125-2.84251594 0.260625-3.08121937 3.60490406-3.35889468 3.40517344-0.28254656 3.6146475-0.49202062 3.89719406-3.89719407 0.28254656-3.40273781 0.47984156-3.60003281 3.58785469-3.60003281 3.09826969 0 3.32722969-0.22652438 3.90450094-3.83873625 0.51150656-3.19569937 0.98647687-3.84117187 2.83520906-3.84117187 2.76457219 0 3.52208906-1.15210781 4.73265469-7.18545094 0.76482469-3.81681469 1.583235-5.18083219 4.02385312-6.72022406 1.87065281-1.17890156 3.31992187-3.05199 3.719385-4.81059938 0.75021-3.28825781 1.90231781-4.28447812 4.97622938-4.30639875 1.82437406-0.01217906 2.16050719-0.53342813 2.16050719-3.37350844 0-4.47690188 7.93078969-5.3440275 9.02443968-0.98647781" fill="#bfbfbf" p-id="11188"></path></svg> \ No newline at end of file diff --git a/web/src/assets/svg/data-flow/total-files-icon-bri.svg b/web/src/assets/svg/data-flow/total-files-icon-bri.svg new file mode 100644 index 000000000..795b5f540 --- /dev/null +++ b/web/src/assets/svg/data-flow/total-files-icon-bri.svg @@ -0,0 +1,6 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0291 4.67969C11.8025 4.67969 12.4787 5.20078 12.6752 5.94844L13.3494 8.50937H31.4275C33.1599 8.50937 34.6158 9.81055 34.8103 11.5316L37.0205 31.1062C37.231 32.9746 35.8877 34.6602 34.0193 34.8711C33.8927 34.8852 33.765 34.8926 33.6377 34.8926H6.30289C4.92476 34.8926 3.79547 33.7988 3.75094 32.4215L3.11734 12.7746H3.115L2.90719 6.4375C2.87633 5.49805 3.61304 4.71133 4.5525 4.68047C4.57086 4.68008 4.58961 4.67969 4.60836 4.67969H11.0291Z" fill="#00BEB4" fill-opacity="0.1"/> +<path d="M11.0291 4.67969C11.8025 4.67969 12.4787 5.20078 12.6752 5.94844L13.349 8.50937H31.4275C33.1599 8.50937 34.6158 9.81055 34.8103 11.5316L37.0205 31.1062C37.231 32.9746 35.8877 34.6602 34.0193 34.8711C33.8927 34.8852 33.765 34.8926 33.6377 34.8926H6.30289C4.92476 34.8926 3.79547 33.7988 3.75094 32.4215L3.11656 12.7742L2.90719 6.4375C2.87633 5.49805 3.61304 4.71133 4.5525 4.68047L4.58023 4.67969H11.0291ZM11.0291 5.10508H4.59078L4.56656 5.10586C3.86187 5.12891 3.30914 5.71914 3.33219 6.42344L3.54195 12.7605L4.17633 32.4078C4.21344 33.5555 5.15445 34.4668 6.30289 34.4668H33.6377C33.749 34.4668 33.8607 34.4605 33.9716 34.448C35.6064 34.2637 36.7822 32.7887 36.5974 31.1539L34.3873 11.5797C34.2173 10.0734 32.9431 8.93516 31.4275 8.93516H13.0209L12.9377 8.61758L12.2638 6.05703C12.1162 5.49609 11.6091 5.10508 11.0291 5.10508Z" fill="#00BEB4"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9.72812 12.7656H36.6539C38.0637 12.7656 39.207 13.9086 39.207 15.3188C39.207 15.4328 39.1992 15.5465 39.184 15.6594L36.9922 31.943C36.7648 33.6324 35.323 34.8934 33.6184 34.8934H6.37969C4.96953 34.8934 3.82617 33.75 3.82617 32.3398C3.82617 32.2102 3.83633 32.0801 3.85586 31.952L6.36367 15.6523C6.61914 13.9914 8.04805 12.7656 9.72812 12.7656Z" fill="#CAF2F0"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98438 14.6172H20.4848C20.899 14.6172 21.2348 14.9529 21.2348 15.3672C21.2348 15.7814 20.899 16.1172 20.4848 16.1172H8.98438C8.57013 16.1172 8.23438 15.7814 8.23438 15.3672C8.23438 14.9529 8.57013 14.6172 8.98438 14.6172Z" fill="#00BEB4"/> +</svg> diff --git a/web/src/assets/svg/data-flow/total-files-icon.svg b/web/src/assets/svg/data-flow/total-files-icon.svg new file mode 100644 index 000000000..702e30909 --- /dev/null +++ b/web/src/assets/svg/data-flow/total-files-icon.svg @@ -0,0 +1,6 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0291 4.68164C11.8025 4.68164 12.4787 5.20273 12.6752 5.95039L13.3494 8.51133H31.4275C33.1599 8.51133 34.6158 9.8125 34.8103 11.5336L37.0205 31.1082C37.231 32.9766 35.8877 34.6621 34.0193 34.873C33.8927 34.8871 33.765 34.8945 33.6377 34.8945H6.30289C4.92476 34.8945 3.79547 33.8008 3.75094 32.4234L3.11734 12.7766H3.115L2.90719 6.43945C2.87633 5.5 3.61304 4.71328 4.5525 4.68242C4.57086 4.68203 4.58961 4.68164 4.60836 4.68164H11.0291Z" fill="#1F3232"/> +<path d="M11.0291 4.68164C11.8025 4.68164 12.4787 5.20273 12.6752 5.95039L13.349 8.51133H31.4275C33.1599 8.51133 34.6158 9.8125 34.8103 11.5336L37.0205 31.1082C37.231 32.9766 35.8877 34.6621 34.0193 34.873C33.8927 34.8871 33.765 34.8945 33.6377 34.8945H6.30289C4.92476 34.8945 3.79547 33.8008 3.75094 32.4234L3.11656 12.7762L2.90719 6.43945C2.87633 5.5 3.61304 4.71328 4.5525 4.68242L4.58023 4.68164H11.0291ZM11.0291 5.10703H4.59078L4.56656 5.10781C3.86187 5.13086 3.30914 5.72109 3.33219 6.42539L3.54195 12.7625L4.17633 32.4098C4.21344 33.5574 5.15445 34.4687 6.30289 34.4687H33.6377C33.749 34.4687 33.8607 34.4625 33.9716 34.45C35.6064 34.2656 36.7822 32.7906 36.5974 31.1559L34.3873 11.5816C34.2173 10.0754 32.9431 8.93711 31.4275 8.93711H13.0209L12.9377 8.61953L12.2638 6.05898C12.1162 5.49805 11.6091 5.10703 11.0291 5.10703Z" fill="#1B3B3C"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9.72812 12.7656H36.6539C38.0637 12.7656 39.207 13.9086 39.207 15.3188C39.207 15.4328 39.1992 15.5465 39.184 15.6594L36.9922 31.943C36.7648 33.6324 35.323 34.8934 33.6184 34.8934H6.37969C4.96953 34.8934 3.82617 33.75 3.82617 32.3398C3.82617 32.2102 3.83633 32.0801 3.85586 31.952L6.36367 15.6523C6.61914 13.9914 8.04805 12.7656 9.72812 12.7656Z" fill="#1B3B3C"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98438 14.6172H20.4848C20.899 14.6172 21.2348 14.9529 21.2348 15.3672C21.2348 15.7814 20.899 16.1172 20.4848 16.1172H8.98438C8.57013 16.1172 8.23438 15.7814 8.23438 15.3672C8.23438 14.9529 8.57013 14.6172 8.98438 14.6172Z" fill="#00BEB4"/> +</svg> diff --git a/web/src/components/chunk-method-dialog/index.tsx b/web/src/components/chunk-method-dialog/index.tsx index 783212f36..8c1bb855e 100644 --- a/web/src/components/chunk-method-dialog/index.tsx +++ b/web/src/components/chunk-method-dialog/index.tsx @@ -18,8 +18,11 @@ import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-reques import { IModalProps } from '@/interfaces/common'; import { IParserConfig } from '@/interfaces/database/document'; import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; +import { + ChunkMethodItem, + ParseTypeItem, +} from '@/pages/dataset/dataset-setting/configuration/common-item'; import { zodResolver } from '@hookform/resolvers/zod'; -import get from 'lodash/get'; import omit from 'lodash/omit'; import {} from 'module'; import { useEffect, useMemo } from 'react'; @@ -30,24 +33,17 @@ import { AutoKeywordsFormField, AutoQuestionsFormField, } from '../auto-keywords-form-field'; +import { DataFlowSelect } from '../data-pipeline-select'; import { DelimiterFormField } from '../delimiter-form-field'; import { EntityTypesFormField } from '../entity-types-form-field'; import { ExcelToHtmlFormField } from '../excel-to-html-form-field'; import { FormContainer } from '../form-container'; import { LayoutRecognizeFormField } from '../layout-recognize-form-field'; import { MaxTokenNumberFormField } from '../max-token-number-from-field'; -import { - UseGraphRagFormField, - showGraphRagItems, -} from '../parse-configuration/graph-rag-form-fields'; -import RaptorFormFields, { - showRaptorParseConfiguration, -} from '../parse-configuration/raptor-form-fields'; import { ButtonLoading } from '../ui/button'; import { Input } from '../ui/input'; -import { RAGFlowSelect } from '../ui/select'; import { DynamicPageRange } from './dynamic-page-range'; -import { useFetchParserListOnMount, useShowAutoKeywords } from './hooks'; +import { useShowAutoKeywords } from './hooks'; import { useDefaultParserValues, useFillDefaultValueOnMount, @@ -62,6 +58,7 @@ interface IProps }> { loading: boolean; parserId: string; + pipelineId?: string; parserConfig: IParserConfig; documentExtension: string; documentId: string; @@ -80,6 +77,7 @@ export function ChunkMethodDialog({ hideModal, onOk, parserId, + pipelineId, documentExtension, visible, parserConfig, @@ -87,8 +85,6 @@ export function ChunkMethodDialog({ }: IProps) { const { t } = useTranslation(); - const { parserList } = useFetchParserListOnMount(documentExtension); - const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration(); const useGraphRag = useMemo(() => { @@ -99,46 +95,59 @@ export function ChunkMethodDialog({ const fillDefaultParserValue = useFillDefaultValueOnMount(); - const FormSchema = z.object({ - parser_id: z - .string() - .min(1, { - message: t('common.pleaseSelect'), - }) - .trim(), - parser_config: z.object({ - task_page_size: z.coerce.number().optional(), - layout_recognize: z.string().optional(), - chunk_token_num: z.coerce.number().optional(), - delimiter: z.string().optional(), - auto_keywords: z.coerce.number().optional(), - auto_questions: z.coerce.number().optional(), - html4excel: z.boolean().optional(), - raptor: z - .object({ - use_raptor: z.boolean().optional(), - prompt: z.string().optional().optional(), - max_token: z.coerce.number().optional(), - threshold: z.coerce.number().optional(), - max_cluster: z.coerce.number().optional(), - random_seed: z.coerce.number().optional(), + const FormSchema = z + .object({ + parseType: z.number(), + parser_id: z + .string() + .min(1, { + message: t('common.pleaseSelect'), }) - .optional(), - graphrag: z.object({ - use_graphrag: z.boolean().optional(), + .trim(), + pipeline_id: z.string().optional(), + parser_config: z.object({ + task_page_size: z.coerce.number().optional(), + layout_recognize: z.string().optional(), + chunk_token_num: z.coerce.number().optional(), + delimiter: z.string().optional(), + auto_keywords: z.coerce.number().optional(), + auto_questions: z.coerce.number().optional(), + html4excel: z.boolean().optional(), + // raptor: z + // .object({ + // use_raptor: z.boolean().optional(), + // prompt: z.string().optional().optional(), + // max_token: z.coerce.number().optional(), + // threshold: z.coerce.number().optional(), + // max_cluster: z.coerce.number().optional(), + // random_seed: z.coerce.number().optional(), + // }) + // .optional(), + // graphrag: z.object({ + // use_graphrag: z.boolean().optional(), + // }), + entity_types: z.array(z.string()).optional(), + pages: z + .array(z.object({ from: z.coerce.number(), to: z.coerce.number() })) + .optional(), }), - entity_types: z.array(z.string()).optional(), - pages: z - .array(z.object({ from: z.coerce.number(), to: z.coerce.number() })) - .optional(), - }), - }); + }) + .superRefine((data, ctx) => { + if (data.parseType === 2 && !data.pipeline_id) { + ctx.addIssue({ + path: ['pipeline_id'], + message: t('common.pleaseSelect'), + code: 'custom', + }); + } + }); const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { - parser_id: parserId, - + parser_id: parserId || '', + pipeline_id: pipelineId || '', + parseType: pipelineId ? 2 : 1, parser_config: defaultParserValues, }, }); @@ -200,17 +209,19 @@ export function ChunkMethodDialog({ const pages = parserConfig?.pages?.map((x) => ({ from: x[0], to: x[1] })) ?? []; form.reset({ - parser_id: parserId, + parser_id: parserId || '', + pipeline_id: pipelineId || '', + parseType: pipelineId ? 2 : 1, parser_config: fillDefaultParserValue({ pages: pages.length > 0 ? pages : [{ from: 1, to: 1024 }], ...omit(parserConfig, 'pages'), - graphrag: { - use_graphrag: get( - parserConfig, - 'graphrag.use_graphrag', - useGraphRag, - ), - }, + // graphrag: { + // use_graphrag: get( + // parserConfig, + // 'graphrag.use_graphrag', + // useGraphRag, + // ), + // }, }), }); } @@ -220,10 +231,20 @@ export function ChunkMethodDialog({ knowledgeDetails.parser_config, parserConfig, parserId, + pipelineId, useGraphRag, visible, ]); - + const parseType = useWatch({ + control: form.control, + name: 'parseType', + defaultValue: pipelineId ? 2 : 1, + }); + useEffect(() => { + if (parseType === 1) { + form.setValue('pipeline_id', ''); + } + }, [parseType, form]); return ( <Dialog open onOpenChange={hideModal}> <DialogContent className="max-w-[50vw]"> @@ -237,7 +258,17 @@ export function ChunkMethodDialog({ id={FormId} > <FormContainer> - <FormField + <ParseTypeItem /> + {parseType === 1 && <ChunkMethodItem></ChunkMethodItem>} + {parseType === 2 && ( + <DataFlowSelect + isMult={false} + // toDataPipeline={navigateToAgents} + formFieldName="pipeline_id" + /> + )} + + {/* <FormField control={form.control} name="parser_id" render={({ field }) => ( @@ -252,9 +283,11 @@ export function ChunkMethodDialog({ <FormMessage /> </FormItem> )} - /> - {showPages && <DynamicPageRange></DynamicPageRange>} - {showPages && layoutRecognize && ( + /> */} + {showPages && parseType === 1 && ( + <DynamicPageRange></DynamicPageRange> + )} + {showPages && parseType === 1 && layoutRecognize && ( <FormField control={form.control} name="parser_config.task_page_size" @@ -279,50 +312,60 @@ export function ChunkMethodDialog({ /> )} </FormContainer> - <FormContainer - show={showOne || showMaxTokenNumber} - className="space-y-3" - > - {showOne && <LayoutRecognizeFormField></LayoutRecognizeFormField>} - {showMaxTokenNumber && ( - <> - <MaxTokenNumberFormField - max={ - selectedTag === DocumentParserType.KnowledgeGraph - ? 8192 * 2 - : 2048 - } - ></MaxTokenNumberFormField> - <DelimiterFormField></DelimiterFormField> - </> - )} - </FormContainer> - <FormContainer - show={showAutoKeywords(selectedTag) || showExcelToHtml} - className="space-y-3" - > - {showAutoKeywords(selectedTag) && ( - <> - <AutoKeywordsFormField></AutoKeywordsFormField> - <AutoQuestionsFormField></AutoQuestionsFormField> - </> - )} - {showExcelToHtml && <ExcelToHtmlFormField></ExcelToHtmlFormField>} - </FormContainer> - {showRaptorParseConfiguration( - selectedTag as DocumentParserType, - ) && ( - <FormContainer> - <RaptorFormFields></RaptorFormFields> - </FormContainer> - )} - {showGraphRagItems(selectedTag as DocumentParserType) && - useGraphRag && ( - <FormContainer> - <UseGraphRagFormField></UseGraphRagFormField> + {parseType === 1 && ( + <> + <FormContainer + show={showOne || showMaxTokenNumber} + className="space-y-3" + > + {showOne && ( + <LayoutRecognizeFormField></LayoutRecognizeFormField> + )} + {showMaxTokenNumber && ( + <> + <MaxTokenNumberFormField + max={ + selectedTag === DocumentParserType.KnowledgeGraph + ? 8192 * 2 + : 2048 + } + ></MaxTokenNumberFormField> + <DelimiterFormField></DelimiterFormField> + </> + )} </FormContainer> - )} - {showEntityTypes && <EntityTypesFormField></EntityTypesFormField>} + <FormContainer + show={showAutoKeywords(selectedTag) || showExcelToHtml} + className="space-y-3" + > + {showAutoKeywords(selectedTag) && ( + <> + <AutoKeywordsFormField></AutoKeywordsFormField> + <AutoQuestionsFormField></AutoQuestionsFormField> + </> + )} + {showExcelToHtml && ( + <ExcelToHtmlFormField></ExcelToHtmlFormField> + )} + </FormContainer> + {/* {showRaptorParseConfiguration( + selectedTag as DocumentParserType, + ) && ( + <FormContainer> + <RaptorFormFields></RaptorFormFields> + </FormContainer> + )} */} + {/* {showGraphRagItems(selectedTag as DocumentParserType) && + useGraphRag && ( + <FormContainer> + <UseGraphRagFormField></UseGraphRagFormField> + </FormContainer> + )} */} + {showEntityTypes && ( + <EntityTypesFormField></EntityTypesFormField> + )} + </> + )} </form> </Form> <DialogFooter> diff --git a/web/src/components/chunk-method-dialog/use-default-parser-values.ts b/web/src/components/chunk-method-dialog/use-default-parser-values.ts index 6b52b50fd..829b98605 100644 --- a/web/src/components/chunk-method-dialog/use-default-parser-values.ts +++ b/web/src/components/chunk-method-dialog/use-default-parser-values.ts @@ -1,7 +1,7 @@ import { IParserConfig } from '@/interfaces/database/document'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { DocumentType } from '../layout-recognize-form-field'; +import { ParseDocumentType } from '../layout-recognize-form-field'; export function useDefaultParserValues() { const { t } = useTranslation(); @@ -9,23 +9,23 @@ export function useDefaultParserValues() { const defaultParserValues = useMemo(() => { const defaultParserValues = { task_page_size: 12, - layout_recognize: DocumentType.DeepDOC, + layout_recognize: ParseDocumentType.DeepDOC, chunk_token_num: 512, delimiter: '\n', auto_keywords: 0, auto_questions: 0, html4excel: false, - raptor: { - use_raptor: false, - prompt: t('knowledgeConfiguration.promptText'), - max_token: 256, - threshold: 0.1, - max_cluster: 64, - random_seed: 0, - }, - graphrag: { - use_graphrag: false, - }, + // raptor: { + // use_raptor: false, + // prompt: t('knowledgeConfiguration.promptText'), + // max_token: 256, + // threshold: 0.1, + // max_cluster: 64, + // random_seed: 0, + // }, + // graphrag: { + // use_graphrag: false, + // }, entity_types: [], pages: [], }; diff --git a/web/src/components/confirm-delete-dialog.tsx b/web/src/components/confirm-delete-dialog.tsx index 412445658..0bd36a194 100644 --- a/web/src/components/confirm-delete-dialog.tsx +++ b/web/src/components/confirm-delete-dialog.tsx @@ -8,7 +8,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; -import { PropsWithChildren } from 'react'; +import { DialogProps } from '@radix-ui/react-dialog'; import { useTranslation } from 'react-i18next'; interface IProps { @@ -24,7 +24,10 @@ export function ConfirmDeleteDialog({ onOk, onCancel, hidden = false, -}: IProps & PropsWithChildren) { + onOpenChange, + open, + defaultOpen, +}: IProps & DialogProps) { const { t } = useTranslation(); if (hidden) { @@ -32,7 +35,11 @@ export function ConfirmDeleteDialog({ } return ( - <AlertDialog> + <AlertDialog + onOpenChange={onOpenChange} + open={open} + defaultOpen={defaultOpen} + > <AlertDialogTrigger asChild>{children}</AlertDialogTrigger> <AlertDialogContent onSelect={(e) => e.preventDefault()} diff --git a/web/src/components/cross-language-form-field.tsx b/web/src/components/cross-language-form-field.tsx index 9f7dc9a05..8855112e5 100644 --- a/web/src/components/cross-language-form-field.tsx +++ b/web/src/components/cross-language-form-field.tsx @@ -22,7 +22,7 @@ const Languages = [ 'Vietnamese', ]; -const options = Languages.map((x) => ({ +export const crossLanguageOptions = Languages.map((x) => ({ label: t('language.' + toLower(x)), value: x, })); @@ -30,11 +30,13 @@ const options = Languages.map((x) => ({ type CrossLanguageItemProps = { name?: string; vertical?: boolean; + label?: string; }; export const CrossLanguageFormField = ({ name = 'prompt_config.cross_languages', vertical = true, + label, }: CrossLanguageItemProps) => { const { t } = useTranslation(); const form = useFormContext(); @@ -53,11 +55,11 @@ export const CrossLanguageFormField = ({ })} > <FormLabel tooltip={t('chat.crossLanguageTip')}> - {t('chat.crossLanguage')} + {label || t('chat.crossLanguage')} </FormLabel> <FormControl> <MultiSelect - options={options} + options={crossLanguageOptions} placeholder={t('fileManager.pleaseSelect')} maxCount={100} {...field} diff --git a/web/src/components/data-pipeline-select/index.tsx b/web/src/components/data-pipeline-select/index.tsx new file mode 100644 index 000000000..76dd15fa1 --- /dev/null +++ b/web/src/components/data-pipeline-select/index.tsx @@ -0,0 +1,120 @@ +import { AgentCategory } from '@/constants/agent'; +import { useTranslate } from '@/hooks/common-hooks'; +import { useFetchAgentList } from '@/hooks/use-agent-request'; +import { buildSelectOptions } from '@/utils/component-util'; +import { ArrowUpRight } from 'lucide-react'; +import { useEffect, useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { SelectWithSearch } from '../originui/select-with-search'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '../ui/form'; +import { MultiSelect } from '../ui/multi-select'; +export interface IDataPipelineSelectNode { + id?: string; + name?: string; + avatar?: string; +} + +interface IProps { + toDataPipeline?: () => void; + formFieldName: string; + isMult?: boolean; + setDataList?: (data: IDataPipelineSelectNode[]) => void; +} + +export function DataFlowSelect(props: IProps) { + const { toDataPipeline, formFieldName, isMult = false, setDataList } = props; + const { t } = useTranslate('knowledgeConfiguration'); + const form = useFormContext(); + const toDataPipLine = () => { + toDataPipeline?.(); + }; + const { data: dataPipelineOptions } = useFetchAgentList({ + canvas_category: AgentCategory.DataflowCanvas, + }); + const options = useMemo(() => { + const option = buildSelectOptions( + dataPipelineOptions?.canvas, + 'id', + 'title', + ); + + return option || []; + }, [dataPipelineOptions]); + + const nodes = useMemo(() => { + return ( + dataPipelineOptions?.canvas?.map((item) => { + return { + id: item?.id, + name: item?.title, + avatar: item?.avatar, + }; + }) || [] + ); + }, [dataPipelineOptions]); + + useEffect(() => { + setDataList?.(nodes); + }, [nodes, setDataList]); + + return ( + <FormField + control={form.control} + name={formFieldName} + render={({ field }) => ( + <FormItem className=" items-center space-y-0 "> + <div className="flex flex-col gap-1"> + <div className="flex gap-2 justify-between "> + <FormLabel + tooltip={t('dataFlowTip')} + className="text-sm text-text-primary whitespace-wrap " + > + {t('dataPipeline')} + </FormLabel> + {toDataPipeline && ( + <div + className="text-sm flex text-text-primary cursor-pointer" + onClick={toDataPipLine} + > + {t('buildItFromScratch')} + <ArrowUpRight size={14} /> + </div> + )} + </div> + + <div className="text-muted-foreground"> + <FormControl> + <> + {!isMult && ( + <SelectWithSearch + {...field} + placeholder={t('dataFlowPlaceholder')} + options={options} + /> + )} + {isMult && ( + <MultiSelect + {...field} + onValueChange={field.onChange} + placeholder={t('dataFlowPlaceholder')} + options={options} + /> + )} + </> + </FormControl> + </div> + </div> + <div className="flex pt-1"> + <FormMessage /> + </div> + </FormItem> + )} + /> + ); +} diff --git a/web/src/components/delimiter-form-field.tsx b/web/src/components/delimiter-form-field.tsx index 2ca5d09a3..ef23fec3b 100644 --- a/web/src/components/delimiter-form-field.tsx +++ b/web/src/components/delimiter-form-field.tsx @@ -16,11 +16,17 @@ interface IProps { } export const DelimiterInput = forwardRef<HTMLInputElement, InputProps & IProps>( - ({ value, onChange, maxLength, defaultValue }, ref) => { - const nextValue = value?.replaceAll('\n', '\\n'); + ({ value, onChange, maxLength, defaultValue, ...props }, ref) => { + const nextValue = value + ?.replaceAll('\n', '\\n') + .replaceAll('\t', '\\t') + .replaceAll('\r', '\\r'); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; - const nextValue = val.replaceAll('\\n', '\n'); + const nextValue = val + .replaceAll('\\n', '\n') + .replaceAll('\\t', '\t') + .replaceAll('\\r', '\r'); onChange?.(nextValue); }; return ( @@ -30,6 +36,7 @@ export const DelimiterInput = forwardRef<HTMLInputElement, InputProps & IProps>( maxLength={maxLength} defaultValue={defaultValue} ref={ref} + {...props} ></Input> ); }, diff --git a/web/src/components/entity-types-form-field.tsx b/web/src/components/entity-types-form-field.tsx index 6008b20b0..0cc6b9f07 100644 --- a/web/src/components/entity-types-form-field.tsx +++ b/web/src/components/entity-types-form-field.tsx @@ -26,7 +26,7 @@ export function EntityTypesFormField({ return ( <FormItem className=" items-center space-y-0 "> <div className="flex items-center"> - <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> + <FormLabel className="text-sm whitespace-nowrap w-1/4"> <span className="text-red-600">*</span> {t('entityTypes')} </FormLabel> <div className="w-3/4"> diff --git a/web/src/components/file-status-badge.tsx b/web/src/components/file-status-badge.tsx index ad681eb34..b57da5f08 100644 --- a/web/src/components/file-status-badge.tsx +++ b/web/src/components/file-status-badge.tsx @@ -1,24 +1,29 @@ // src/pages/dataset/file-logs/file-status-badge.tsx +import { RunningStatus } from '@/pages/dataset/dataset/constant'; import { FC } from 'react'; - +/** + * params: status: 0 not run yet 1 running, 2 cancel, 3 success, 4 fail + */ interface StatusBadgeProps { - status: 'Success' | 'Failed' | 'Running' | 'Pending'; + // status: 'Success' | 'Failed' | 'Running' | 'Pending'; + status: RunningStatus; + name?: string; } -const FileStatusBadge: FC<StatusBadgeProps> = ({ status }) => { +const FileStatusBadge: FC<StatusBadgeProps> = ({ status, name }) => { const getStatusColor = () => { // #3ba05c → rgb(59, 160, 92) // state-success // #d8494b → rgb(216, 73, 75) // state-error // #00beb4 → rgb(0, 190, 180) // accent-primary // #faad14 → rgb(250, 173, 20) // state-warning switch (status) { - case 'Success': + case RunningStatus.DONE: return `bg-[rgba(59,160,92,0.1)] text-state-success`; - case 'Failed': + case RunningStatus.FAIL: return `bg-[rgba(216,73,75,0.1)] text-state-error`; - case 'Running': + case RunningStatus.RUNNING: return `bg-[rgba(0,190,180,0.1)] text-accent-primary`; - case 'Pending': + case RunningStatus.UNSTART: return `bg-[rgba(250,173,20,0.1)] text-state-warning`; default: return 'bg-gray-500/10 text-white'; @@ -31,13 +36,13 @@ const FileStatusBadge: FC<StatusBadgeProps> = ({ status }) => { // #00beb4 → rgb(0, 190, 180) // accent-primary // #faad14 → rgb(250, 173, 20) // state-warning switch (status) { - case 'Success': + case RunningStatus.DONE: return `bg-[rgba(59,160,92,1)] text-state-success`; - case 'Failed': + case RunningStatus.FAIL: return `bg-[rgba(216,73,75,1)] text-state-error`; - case 'Running': + case RunningStatus.RUNNING: return `bg-[rgba(0,190,180,1)] text-accent-primary`; - case 'Pending': + case RunningStatus.UNSTART: return `bg-[rgba(250,173,20,1)] text-state-warning`; default: return 'bg-gray-500/10 text-white'; @@ -46,10 +51,10 @@ const FileStatusBadge: FC<StatusBadgeProps> = ({ status }) => { return ( <span - className={`inline-flex items-center w-[75px] px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(0.1)}`} + className={`inline-flex items-center w-[75px] px-2 py-1 rounded-full text-xs font-medium ${getStatusColor()}`} > <div className={`w-1 h-1 mr-1 rounded-full ${getBgStatusColor()}`}></div> - {status} + {name || ''} </span> ); }; diff --git a/web/src/components/home-card.tsx b/web/src/components/home-card.tsx index 8782e3f00..ff355dae2 100644 --- a/web/src/components/home-card.tsx +++ b/web/src/components/home-card.tsx @@ -13,8 +13,15 @@ interface IProps { onClick?: () => void; moreDropdown: React.ReactNode; sharedBadge?: ReactNode; + icon?: React.ReactNode; } -export function HomeCard({ data, onClick, moreDropdown, sharedBadge }: IProps) { +export function HomeCard({ + data, + onClick, + moreDropdown, + sharedBadge, + icon, +}: IProps) { return ( <Card className="bg-bg-card border-colors-outline-neutral-standard" @@ -32,10 +39,13 @@ export function HomeCard({ data, onClick, moreDropdown, sharedBadge }: IProps) { /> </div> <div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]"> - <section className="flex justify-between"> - <div className="text-[20px] font-bold w-80% leading-5 text-ellipsis overflow-hidden"> - {data.name} - </div> + <section className="flex justify-between w-full"> + <section className="flex gap-1 items-center w-full"> + <div className="text-[20px] font-bold w-80% leading-5 text-ellipsis overflow-hidden"> + {data.name} + </div> + {icon} + </section> {moreDropdown} </section> diff --git a/web/src/components/icon-font.tsx b/web/src/components/icon-font.tsx index a0b9a8e12..7c6436d90 100644 --- a/web/src/components/icon-font.tsx +++ b/web/src/components/icon-font.tsx @@ -4,6 +4,7 @@ import { getExtension } from '@/utils/document-util'; type IconFontType = { name: string; + className?: string; }; @@ -13,6 +14,23 @@ export const IconFont = ({ name, className }: IconFontType) => ( </svg> ); +export function IconFontFill({ + name, + className, + isFill = true, +}: IconFontType & { isFill?: boolean }) { + return ( + <span className={cn('size-4', className)}> + <svg + className={cn('size-4', className)} + style={{ fill: isFill ? 'currentColor' : '' }} + > + <use xlinkHref={`#icon-${name}`} /> + </svg> + </span> + ); +} + export function FileIcon({ name, className, diff --git a/web/src/components/layout-recognize-form-field.tsx b/web/src/components/layout-recognize-form-field.tsx index e8e0ed09d..0e5b660bb 100644 --- a/web/src/components/layout-recognize-form-field.tsx +++ b/web/src/components/layout-recognize-form-field.tsx @@ -1,9 +1,11 @@ import { LlmModelType } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks'; +import { cn } from '@/lib/utils'; import { camelCase } from 'lodash'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; +import { SelectWithSearch } from './originui/select-with-search'; import { FormControl, FormField, @@ -11,24 +13,36 @@ import { FormLabel, FormMessage, } from './ui/form'; -import { RAGFlowSelect } from './ui/select'; -export const enum DocumentType { +export const enum ParseDocumentType { DeepDOC = 'DeepDOC', PlainText = 'Plain Text', } -export function LayoutRecognizeFormField() { +export function LayoutRecognizeFormField({ + name = 'parser_config.layout_recognize', + horizontal = true, + optionsWithoutLLM, + label, +}: { + name?: string; + horizontal?: boolean; + optionsWithoutLLM?: { value: string; label: string }[]; + label?: ReactNode; +}) { const form = useFormContext(); const { t } = useTranslate('knowledgeDetails'); const allOptions = useSelectLlmOptionsByModelType(); const options = useMemo(() => { - const list = [DocumentType.DeepDOC, DocumentType.PlainText].map((x) => ({ - label: x === DocumentType.PlainText ? t(camelCase(x)) : 'DeepDoc', - value: x, - })); + const list = optionsWithoutLLM + ? optionsWithoutLLM + : [ParseDocumentType.DeepDOC, ParseDocumentType.PlainText].map((x) => ({ + label: + x === ParseDocumentType.PlainText ? t(camelCase(x)) : 'DeepDoc', + value: x, + })); const image2TextList = allOptions[LlmModelType.Image2text].map((x) => { return { @@ -48,38 +62,40 @@ export function LayoutRecognizeFormField() { }); return [...list, ...image2TextList]; - }, [allOptions, t]); + }, [allOptions, optionsWithoutLLM, t]); return ( <FormField control={form.control} - name="parser_config.layout_recognize" + name={name} render={({ field }) => { - if (typeof field.value === 'undefined') { - // default value set - form.setValue( - 'parser_config.layout_recognize', - form.formState.defaultValues?.parser_config?.layout_recognize ?? - 'DeepDOC', - ); - } return ( - <FormItem className=" items-center space-y-0 "> - <div className="flex items-center"> + <FormItem className={'items-center space-y-0 '}> + <div + className={cn('flex', { + 'flex-col ': !horizontal, + 'items-center': horizontal, + })} + > <FormLabel tooltip={t('layoutRecognizeTip')} - className="text-sm text-muted-foreground whitespace-wrap w-1/4" + className={cn('text-sm text-muted-foreground whitespace-wrap', { + ['w-1/4']: horizontal, + })} > - {t('layoutRecognize')} + {label || t('layoutRecognize')} </FormLabel> - <div className="w-3/4"> + <div className={horizontal ? 'w-3/4' : 'w-full'}> <FormControl> - <RAGFlowSelect {...field} options={options}></RAGFlowSelect> + <SelectWithSearch + {...field} + options={options} + ></SelectWithSearch> </FormControl> </div> </div> <div className="flex pt-1"> - <div className="w-1/4"></div> + <div className={horizontal ? 'w-1/4' : 'w-full'}></div> <FormMessage /> </div> </FormItem> diff --git a/web/src/components/llm-setting-items/llm-form-field.tsx b/web/src/components/llm-setting-items/llm-form-field.tsx new file mode 100644 index 000000000..52aceb4a3 --- /dev/null +++ b/web/src/components/llm-setting-items/llm-form-field.tsx @@ -0,0 +1,25 @@ +import { LlmModelType } from '@/constants/knowledge'; +import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; +import { useTranslation } from 'react-i18next'; +import { SelectWithSearch } from '../originui/select-with-search'; +import { RAGFlowFormItem } from '../ragflow-form'; + +type LLMFormFieldProps = { + options?: any[]; + name?: string; +}; + +export function LLMFormField({ options, name }: LLMFormFieldProps) { + const { t } = useTranslation(); + + const modelOptions = useComposeLlmOptionsByModelTypes([ + LlmModelType.Chat, + LlmModelType.Image2text, + ]); + + return ( + <RAGFlowFormItem name={name || 'llm_id'} label={t('chat.model')}> + <SelectWithSearch options={options || modelOptions}></SelectWithSearch> + </RAGFlowFormItem> + ); +} diff --git a/web/src/components/llm-setting-items/next.tsx b/web/src/components/llm-setting-items/next.tsx index 14e6e532c..8d21bf9a8 100644 --- a/web/src/components/llm-setting-items/next.tsx +++ b/web/src/components/llm-setting-items/next.tsx @@ -1,11 +1,9 @@ -import { LlmModelType, ModelVariableType } from '@/constants/knowledge'; +import { ModelVariableType } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; -import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; import { camelCase } from 'lodash'; import { useCallback } from 'react'; import { useFormContext } from 'react-hook-form'; import { z } from 'zod'; -import { SelectWithSearch } from '../originui/select-with-search'; import { FormControl, FormField, @@ -20,6 +18,7 @@ import { SelectTrigger, SelectValue, } from '../ui/select'; +import { LLMFormField } from './llm-form-field'; import { SliderInputSwitchFormField } from './slider'; import { useHandleFreedomChange } from './use-watch-change'; @@ -61,11 +60,6 @@ export function LlmSettingFieldItems({ const form = useFormContext(); const { t } = useTranslate('chat'); - const modelOptions = useComposeLlmOptionsByModelTypes([ - LlmModelType.Chat, - LlmModelType.Image2text, - ]); - const getFieldWithPrefix = useCallback( (name: string) => { return prefix ? `${prefix}.${name}` : name; @@ -82,22 +76,7 @@ export function LlmSettingFieldItems({ return ( <div className="space-y-5"> - <FormField - control={form.control} - name={'llm_id'} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('model')}</FormLabel> - <FormControl> - <SelectWithSearch - options={options || modelOptions} - {...field} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + <LLMFormField options={options}></LLMFormField> <FormField control={form.control} name={'parameter'} diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx index 899fb529c..031fdddb1 100644 --- a/web/src/components/originui/select-with-search.tsx +++ b/web/src/components/originui/select-with-search.tsx @@ -45,8 +45,26 @@ export type SelectWithSearchFlagProps = { onChange?(value: string): void; triggerClassName?: string; allowClear?: boolean; + disabled?: boolean; + placeholder?: string; }; +function findLabelWithoutOptions( + options: SelectWithSearchFlagOptionType[], + value: string, +) { + return options.find((opt) => opt.value === value)?.label || ''; +} + +function findLabelWithOptions( + options: SelectWithSearchFlagOptionType[], + value: string, +) { + return options + .map((group) => group?.options?.find((item) => item.value === value)) + .filter(Boolean)[0]?.label; +} + export const SelectWithSearch = forwardRef< React.ElementRef<typeof Button>, SelectWithSearchFlagProps @@ -58,6 +76,8 @@ export const SelectWithSearch = forwardRef< options = [], triggerClassName, allowClear = false, + disabled = false, + placeholder = t('common.selectPlaceholder'), }, ref, ) => { @@ -65,6 +85,28 @@ export const SelectWithSearch = forwardRef< const [open, setOpen] = useState<boolean>(false); const [value, setValue] = useState<string>(''); + const selectLabel = useMemo(() => { + if (options.every((x) => x.options === undefined)) { + return findLabelWithoutOptions(options, value); + } else if (options.every((x) => Array.isArray(x.options))) { + return findLabelWithOptions(options, value); + } else { + // Some have options, some don't + const optionsWithOptions = options.filter((x) => + Array.isArray(x.options), + ); + const optionsWithoutOptions = options.filter( + (x) => x.options === undefined, + ); + + const label = findLabelWithOptions(optionsWithOptions, value); + if (label) { + return label; + } + return findLabelWithoutOptions(optionsWithoutOptions, value); + } + }, [options, value]); + const handleSelect = useCallback( (val: string) => { setValue(val); @@ -86,16 +128,7 @@ export const SelectWithSearch = forwardRef< useEffect(() => { setValue(val); }, [val]); - const selectLabel = useMemo(() => { - const optionTemp = options[0]; - if (optionTemp?.options) { - return options - .map((group) => group?.options?.find((item) => item.value === value)) - .filter(Boolean)[0]?.label; - } else { - return options.find((opt) => opt.value === value)?.label || ''; - } - }, [options, value]); + return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> @@ -105,6 +138,7 @@ export const SelectWithSearch = forwardRef< role="combobox" aria-expanded={open} ref={ref} + disabled={disabled} className={cn( 'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto', triggerClassName, @@ -115,9 +149,7 @@ export const SelectWithSearch = forwardRef< <span className="leading-none truncate">{selectLabel}</span> </span> ) : ( - <span className="text-muted-foreground"> - {t('common.selectPlaceholder')} - </span> + <span className="text-muted-foreground">{placeholder}</span> )} <div className="flex items-center justify-between"> {value && allowClear && ( diff --git a/web/src/components/originui/timeline.tsx b/web/src/components/originui/timeline.tsx index 57694ee3b..4c1b48526 100644 --- a/web/src/components/originui/timeline.tsx +++ b/web/src/components/originui/timeline.tsx @@ -1,7 +1,8 @@ 'use client'; import { cn } from '@/lib/utils'; -import { parseColorToRGBA } from '@/utils/common-util'; +import { TimelineNodeType } from '@/pages/dataflow-result/constant'; +import { parseColorToRGB } from '@/utils/common-util'; import { Slot } from '@radix-ui/react-slot'; import * as React from 'react'; @@ -220,6 +221,8 @@ interface TimelineNode completed?: boolean; clickable?: boolean; activeStyle?: TimelineIndicatorNodeProps; + detail?: any; + type?: TimelineNodeType; } interface CustomTimelineProps extends React.HTMLAttributes<HTMLDivElement> { @@ -243,7 +246,7 @@ const CustomTimeline = ({ orientation = 'horizontal', lineStyle = 'solid', lineColor = 'var(--text-secondary)', - indicatorColor = 'var(--accent-primary)', + indicatorColor = 'rgb(var(--accent-primary))', defaultValue = 1, className, activeStyle, @@ -251,8 +254,7 @@ const CustomTimeline = ({ }: CustomTimelineProps) => { const [internalActiveStep, setInternalActiveStep] = React.useState(defaultValue); - const _lineColor = `rgb(${parseColorToRGBA(lineColor)})`; - console.log(lineColor, _lineColor); + const _lineColor = `rgb(${parseColorToRGB(lineColor)})`; const currentActiveStep = activeStep ?? internalActiveStep; const handleStepChange = (step: number, id: string | number) => { @@ -261,7 +263,7 @@ const CustomTimeline = ({ } onStepChange?.(step, id); }; - const [r, g, b] = parseColorToRGBA(indicatorColor); + const [r, g, b] = parseColorToRGB(indicatorColor); return ( <Timeline value={currentActiveStep} @@ -284,8 +286,6 @@ const CustomTimeline = ({ typeof _nodeSizeTemp === 'number' ? `${_nodeSizeTemp}px` : _nodeSizeTemp; - console.log('icon-size', nodeSize, node.nodeSize, _nodeSize); - // const activeStyle = _activeStyle || {}; return ( <TimelineItem @@ -372,11 +372,10 @@ const CustomTimeline = ({ )} </TimelineIndicator> - <TimelineHeader> - {node.date && <TimelineDate>{node.date}</TimelineDate>} + <TimelineHeader className="transform -translate-x-[40%] text-center"> <TimelineTitle className={cn( - 'text-sm font-medium', + 'text-sm font-medium -ml-1', isActive && _activeStyle.textColor ? `text-${_activeStyle.textColor}` : '', @@ -387,6 +386,7 @@ const CustomTimeline = ({ > {node.title} </TimelineTitle> + {node.date && <TimelineDate>{node.date}</TimelineDate>} </TimelineHeader> {node.content && <TimelineContent>{node.content}</TimelineContent>} </TimelineItem> diff --git a/web/src/components/parse-configuration/graph-rag-form-fields.tsx b/web/src/components/parse-configuration/graph-rag-form-fields.tsx index 21a239938..56e631d4a 100644 --- a/web/src/components/parse-configuration/graph-rag-form-fields.tsx +++ b/web/src/components/parse-configuration/graph-rag-form-fields.tsx @@ -1,6 +1,11 @@ import { DocumentParserType } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; import { cn } from '@/lib/utils'; +import { + GenerateLogButton, + GenerateType, + IGenerateLogButtonProps, +} from '@/pages/dataset/dataset/generate-button/generate'; import { upperFirst } from 'lodash'; import { useCallback, useMemo } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; @@ -47,9 +52,17 @@ export const showGraphRagItems = (parserId: DocumentParserType | undefined) => { type GraphRagItemsProps = { marginBottom?: boolean; className?: string; + data: IGenerateLogButtonProps; + onDelete?: () => void; }; -export function UseGraphRagFormField() { +export function UseGraphRagFormField({ + data, + onDelete, +}: { + data: IGenerateLogButtonProps; + onDelete?: () => void; +}) { const form = useFormContext(); const { t } = useTranslate('knowledgeConfiguration'); @@ -62,16 +75,23 @@ export function UseGraphRagFormField() { <div className="flex items-center gap-1"> <FormLabel tooltip={t('useGraphRagTip')} - className="text-sm text-muted-foreground whitespace-break-spaces w-1/4" + className="text-sm whitespace-break-spaces w-1/4" > {t('useGraphRag')} </FormLabel> <div className="w-3/4"> <FormControl> - <Switch + {/* <Switch checked={field.value} onCheckedChange={field.onChange} - ></Switch> + ></Switch> */} + <GenerateLogButton + {...data} + onDelete={onDelete} + className="w-full text-text-secondary" + status={1} + type={GenerateType.KnowledgeGraph} + /> </FormControl> </div> </div> @@ -89,6 +109,8 @@ export function UseGraphRagFormField() { const GraphRagItems = ({ marginBottom = false, className = 'p-10', + data, + onDelete, }: GraphRagItemsProps) => { const { t } = useTranslate('knowledgeConfiguration'); const form = useFormContext(); @@ -114,7 +136,10 @@ const GraphRagItems = ({ return ( <FormContainer className={cn({ 'mb-4': marginBottom }, className)}> - <UseGraphRagFormField></UseGraphRagFormField> + <UseGraphRagFormField + data={data} + onDelete={onDelete} + ></UseGraphRagFormField> {useRaptor && ( <> <EntityTypesFormField name="parser_config.graphrag.entity_types"></EntityTypesFormField> @@ -125,7 +150,7 @@ const GraphRagItems = ({ <FormItem className=" items-center space-y-0 "> <div className="flex items-center"> <FormLabel - className="text-sm text-muted-foreground whitespace-nowrap w-1/4" + className="text-sm whitespace-nowrap w-1/4" tooltip={renderWideTooltip( <div dangerouslySetInnerHTML={{ @@ -161,7 +186,7 @@ const GraphRagItems = ({ <div className="flex items-center"> <FormLabel tooltip={renderWideTooltip('resolutionTip')} - className="text-sm text-muted-foreground whitespace-nowrap w-1/4" + className="text-sm whitespace-nowrap w-1/4" > {t('resolution')} </FormLabel> @@ -190,7 +215,7 @@ const GraphRagItems = ({ <div className="flex items-center"> <FormLabel tooltip={renderWideTooltip('communityTip')} - className="text-sm text-muted-foreground whitespace-nowrap w-1/4" + className="text-sm whitespace-nowrap w-1/4" > {t('community')} </FormLabel> @@ -210,6 +235,18 @@ const GraphRagItems = ({ </FormItem> )} /> + {/* {showGenerateItem && ( + <div className="w-full flex items-center"> + <div className="text-sm whitespace-nowrap w-1/4"> + {t('extractKnowledgeGraph')} + </div> + <GenerateLogButton + className="w-3/4 text-text-secondary" + status={1} + type={GenerateType.KnowledgeGraph} + /> + </div> + )} */} </> )} </FormContainer> diff --git a/web/src/components/parse-configuration/raptor-form-fields.tsx b/web/src/components/parse-configuration/raptor-form-fields.tsx index 9f3fb2061..4571288c7 100644 --- a/web/src/components/parse-configuration/raptor-form-fields.tsx +++ b/web/src/components/parse-configuration/raptor-form-fields.tsx @@ -1,12 +1,16 @@ import { FormLayout } from '@/constants/form'; import { DocumentParserType } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; +import { + GenerateLogButton, + GenerateType, + IGenerateLogButtonProps, +} from '@/pages/dataset/dataset/generate-button/generate'; import random from 'lodash/random'; -import { Plus } from 'lucide-react'; +import { Shuffle } from 'lucide-react'; import { useCallback } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { SliderInputFormField } from '../slider-input-form-field'; -import { Button } from '../ui/button'; import { FormControl, FormField, @@ -14,8 +18,7 @@ import { FormLabel, FormMessage, } from '../ui/form'; -import { Input } from '../ui/input'; -import { Switch } from '../ui/switch'; +import { ExpandedInput } from '../ui/input'; import { Textarea } from '../ui/textarea'; export const excludedParseMethods = [ @@ -53,7 +56,13 @@ const Prompt = 'parser_config.raptor.prompt'; // The three types "table", "resume" and "one" do not display this configuration. -const RaptorFormFields = () => { +const RaptorFormFields = ({ + data, + onDelete, +}: { + data: IGenerateLogButtonProps; + onDelete: () => void; +}) => { const form = useFormContext(); const { t } = useTranslate('knowledgeConfiguration'); const useRaptor = useWatch({ name: UseRaptorField }); @@ -93,7 +102,7 @@ const RaptorFormFields = () => { <div className="flex items-center gap-1"> <FormLabel tooltip={t('useRaptorTip')} - className="text-sm text-muted-foreground w-1/4 whitespace-break-spaces" + className="text-sm w-1/4 whitespace-break-spaces" > <div className="w-auto xl:w-20 2xl:w-24 3xl:w-28 4xl:w-auto "> {t('useRaptor')} @@ -101,13 +110,13 @@ const RaptorFormFields = () => { </FormLabel> <div className="w-3/4"> <FormControl> - <Switch - checked={field.value} - onCheckedChange={(e) => { - changeRaptor(e); - field.onChange(e); - }} - ></Switch> + <GenerateLogButton + {...data} + onDelete={onDelete} + className="w-full text-text-secondary" + status={1} + type={GenerateType.Raptor} + /> </FormControl> </div> </div> @@ -130,7 +139,7 @@ const RaptorFormFields = () => { <div className="flex items-start"> <FormLabel tooltip={t('promptTip')} - className="text-sm text-muted-foreground whitespace-nowrap w-1/4" + className="text-sm whitespace-nowrap w-1/4" > {t('prompt')} </FormLabel> @@ -185,21 +194,23 @@ const RaptorFormFields = () => { render={({ field }) => ( <FormItem className=" items-center space-y-0 "> <div className="flex items-center"> - <FormLabel className="text-sm text-muted-foreground whitespace-wrap w-1/4"> + <FormLabel className="text-sm whitespace-wrap w-1/4"> {t('randomSeed')} </FormLabel> <div className="w-3/4"> <FormControl defaultValue={0}> - <div className="flex gap-4 items-center"> - <Input {...field} defaultValue={0} type="number" /> - <Button - size={'sm'} - onClick={handleGenerate} - type={'button'} - > - <Plus /> - </Button> - </div> + <ExpandedInput + {...field} + className="w-full" + defaultValue={0} + type="number" + suffix={ + <Shuffle + className="size-3.5 cursor-pointer" + onClick={handleGenerate} + /> + } + /> </FormControl> </div> </div> diff --git a/web/src/components/ragflow-form.tsx b/web/src/components/ragflow-form.tsx index 4151e6fef..28d1138b9 100644 --- a/web/src/components/ragflow-form.tsx +++ b/web/src/components/ragflow-form.tsx @@ -11,11 +11,12 @@ import { ControllerRenderProps, useFormContext } from 'react-hook-form'; type RAGFlowFormItemProps = { name: string; - label: ReactNode; + label?: ReactNode; tooltip?: ReactNode; children: ReactNode | ((field: ControllerRenderProps) => ReactNode); horizontal?: boolean; required?: boolean; + labelClassName?: string; }; export function RAGFlowFormItem({ @@ -25,6 +26,7 @@ export function RAGFlowFormItem({ children, horizontal = false, required = false, + labelClassName, }: RAGFlowFormItemProps) { const form = useFormContext(); return ( @@ -37,13 +39,15 @@ export function RAGFlowFormItem({ 'flex items-center': horizontal, })} > - <FormLabel - required={required} - tooltip={tooltip} - className={cn({ 'w-1/4': horizontal })} - > - {label} - </FormLabel> + {label && ( + <FormLabel + required={required} + tooltip={tooltip} + className={cn({ 'w-1/4': horizontal }, labelClassName)} + > + {label} + </FormLabel> + )} <FormControl> {typeof children === 'function' ? children(field) diff --git a/web/src/components/slider-input-form-field.tsx b/web/src/components/slider-input-form-field.tsx index c91007f22..f496ba1c5 100644 --- a/web/src/components/slider-input-form-field.tsx +++ b/web/src/components/slider-input-form-field.tsx @@ -54,8 +54,7 @@ export function SliderInputFormField({ <FormLabel tooltip={tooltip} className={cn({ - 'text-sm text-muted-foreground whitespace-break-spaces w-1/4': - isHorizontal, + 'text-sm whitespace-break-spaces w-1/4': isHorizontal, })} > {label} diff --git a/web/src/components/ui/dual-range-slider.tsx b/web/src/components/ui/dual-range-slider.tsx index a0ddd83db..e2fbb303c 100644 --- a/web/src/components/ui/dual-range-slider.tsx +++ b/web/src/components/ui/dual-range-slider.tsx @@ -28,7 +28,7 @@ const DualRangeSlider = React.forwardRef< )} {...props} > - <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> + <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-border-button"> <SliderPrimitive.Range className="absolute h-full bg-accent-primary" /> </SliderPrimitive.Track> {initialValue.map((value, index) => ( diff --git a/web/src/components/ui/modal/modal.tsx b/web/src/components/ui/modal/modal.tsx index 9126f826a..fff5216b8 100644 --- a/web/src/components/ui/modal/modal.tsx +++ b/web/src/components/ui/modal/modal.tsx @@ -31,6 +31,7 @@ export interface ModalProps { export interface ModalType extends FC<ModalProps> { show: typeof modalIns.show; hide: typeof modalIns.hide; + destroy: typeof modalIns.destroy; } const Modal: ModalType = ({ @@ -76,20 +77,20 @@ const Modal: ModalType = ({ const handleCancel = useCallback(() => { onOpenChange?.(false); onCancel?.(); - }, [onOpenChange, onCancel]); + }, [onCancel, onOpenChange]); const handleOk = useCallback(() => { onOpenChange?.(true); onOk?.(); - }, [onOpenChange, onOk]); + }, [onOk, onOpenChange]); const handleChange = (open: boolean) => { onOpenChange?.(open); console.log('open', open, onOpenChange); if (open) { - handleOk(); + onOk?.(); } if (!open) { - handleCancel(); + onCancel?.(); } }; const footEl = useMemo(() => { @@ -177,7 +178,7 @@ const Modal: ModalType = ({ <DialogPrimitive.Close asChild> <button type="button" - className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted" + className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted focus-visible:outline-none" > {closeIcon} </button> @@ -187,7 +188,7 @@ const Modal: ModalType = ({ )} {/* content */} - <div className="py-2 px-6 overflow-y-auto max-h-[80vh] focus-visible:!outline-none"> + <div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[80vh] focus-visible:!outline-none"> {destroyOnClose && !open ? null : children} </div> @@ -208,5 +209,6 @@ Modal.show = modalIns return modalIns.show; }; Modal.hide = modalIns.hide; +Modal.destroy = modalIns.destroy; export { Modal }; diff --git a/web/src/components/ui/radio.tsx b/web/src/components/ui/radio.tsx index 915056cf8..5db0fca43 100644 --- a/web/src/components/ui/radio.tsx +++ b/web/src/components/ui/radio.tsx @@ -49,7 +49,7 @@ function Radio({ value, checked, disabled, onChange, children }: RadioProps) { > <span className={cn( - 'flex h-4 w-4 items-center justify-center rounded-full border border-input transition-colors', + 'flex h-4 w-4 items-center justify-center rounded-full border border-border transition-colors', 'peer ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', isChecked && 'border-primary bg-primary/10', mergedDisabled && 'border-muted', diff --git a/web/src/components/ui/tooltip.tsx b/web/src/components/ui/tooltip.tsx index 15bbc481c..1ec8529d7 100644 --- a/web/src/components/ui/tooltip.tsx +++ b/web/src/components/ui/tooltip.tsx @@ -33,7 +33,12 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => { return ( <Tooltip> - <TooltipTrigger tabIndex={-1}> + <TooltipTrigger + tabIndex={-1} + onClick={(e) => { + e.preventDefault(); // Prevent clicking the tooltip from triggering form save + }} + > <Info className="size-3 ml-2" /> </TooltipTrigger> <TooltipContent> @@ -107,7 +112,7 @@ export const AntToolTip: React.FC<AntToolTipProps> = ({ {visible && title && ( <div className={cn( - 'absolute z-50 px-2.5 py-2 text-xs text-text-primary bg-muted rounded-sm shadow-sm whitespace-wrap', + 'absolute z-50 px-2.5 py-2 text-xs text-text-primary bg-muted rounded-sm shadow-sm whitespace-wrap w-max', getPlacementClasses(), className, )} diff --git a/web/src/constants/agent.ts b/web/src/constants/agent.ts index 35ac2a6f9..7e9340885 100644 --- a/web/src/constants/agent.ts +++ b/web/src/constants/agent.ts @@ -1,3 +1,6 @@ +import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat'; +import { ChatVariableEnabledField, variableEnabledFieldMap } from './chat'; + export enum ProgrammingLanguage { Python = 'python', Javascript = 'javascript', @@ -26,3 +29,26 @@ export enum AgentGlobals { } export const AgentGlobalsSysQueryWithBrace = `{${AgentGlobals.SysQuery}}`; + +export const variableCheckBoxFieldMap = Object.keys( + variableEnabledFieldMap, +).reduce<Record<string, boolean>>((pre, cur) => { + pre[cur] = setInitialChatVariableEnabledFieldValue( + cur as ChatVariableEnabledField, + ); + return pre; +}, {}); + +export const initialLlmBaseValues = { + ...variableCheckBoxFieldMap, + temperature: 0.1, + top_p: 0.3, + frequency_penalty: 0.7, + presence_penalty: 0.4, + max_tokens: 256, +}; + +export enum AgentCategory { + AgentCanvas = 'agent_canvas', + DataflowCanvas = 'dataflow_canvas', +} diff --git a/web/src/constants/knowledge.ts b/web/src/constants/knowledge.ts index 6839f383f..0d4d76f7e 100644 --- a/web/src/constants/knowledge.ts +++ b/web/src/constants/knowledge.ts @@ -15,6 +15,14 @@ export enum RunningStatus { FAIL = '4', // need to refresh } +export const RunningStatusMap = { + [RunningStatus.UNSTART]: 'Pending', + [RunningStatus.RUNNING]: 'Running', + [RunningStatus.CANCEL]: 'Cancel', + [RunningStatus.DONE]: 'Success', + [RunningStatus.FAIL]: 'Failed', +}; + export enum ModelVariableType { Improvise = 'Improvise', Precise = 'Precise', @@ -57,6 +65,7 @@ export enum LlmModelType { export enum KnowledgeSearchParams { DocumentId = 'doc_id', KnowledgeId = 'id', + Type = 'type', } export enum DocumentType { diff --git a/web/src/hooks/flow-hooks.ts b/web/src/hooks/flow-hooks.ts index 55abc2941..5acfd0649 100644 --- a/web/src/hooks/flow-hooks.ts +++ b/web/src/hooks/flow-hooks.ts @@ -1,79 +1,14 @@ -import { ResponseType } from '@/interfaces/database/base'; -import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; +import { DSL, IFlow } from '@/interfaces/database/flow'; import { IDebugSingleRequestBody } from '@/interfaces/request/flow'; import i18n from '@/locales/config'; import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks'; -import { BeginId } from '@/pages/flow/constant'; import flowService from '@/services/flow-service'; import { buildMessageListWithUuid } from '@/utils/chat'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { message } from 'antd'; import { set } from 'lodash'; import get from 'lodash/get'; -import { useTranslation } from 'react-i18next'; import { useParams } from 'umi'; -import { v4 as uuid } from 'uuid'; - -export const EmptyDsl = { - graph: { - nodes: [ - { - id: BeginId, - type: 'beginNode', - position: { - x: 50, - y: 200, - }, - data: { - label: 'Begin', - name: 'begin', - }, - sourcePosition: 'left', - targetPosition: 'right', - }, - ], - edges: [], - }, - components: { - begin: { - obj: { - component_name: 'Begin', - params: {}, - }, - downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id - upstream: [], // edge source is upstream, edge target is current node id - }, - }, - messages: [], - reference: [], - history: [], - path: [], - answer: [], -}; - -export const useFetchFlowTemplates = (): ResponseType<IFlowTemplate[]> => { - const { t } = useTranslation(); - - const { data } = useQuery({ - queryKey: ['fetchFlowTemplates'], - initialData: [], - queryFn: async () => { - const { data } = await flowService.listTemplates(); - if (Array.isArray(data?.data)) { - data.data.unshift({ - id: uuid(), - title: t('flow.blank'), - description: t('flow.createFromNothing'), - dsl: EmptyDsl, - }); - } - - return data; - }, - }); - - return data; -}; export const useFetchFlowList = (): { data: IFlow[]; loading: boolean } => { const { data, isFetching: loading } = useQuery({ diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index b403b37ec..642b8c3c0 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -1,3 +1,4 @@ +import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface'; import { Routes } from '@/routes'; import { useCallback } from 'react'; import { useNavigate, useParams, useSearchParams } from 'umi'; @@ -18,7 +19,14 @@ export const useNavigatePage = () => { const navigateToDataset = useCallback( (id: string) => () => { - navigate(`${Routes.Dataset}/${id}`); + navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`); + }, + [navigate], + ); + + const navigateToDataFile = useCallback( + (id: string) => () => { + navigate(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`); }, [navigate], ); @@ -61,6 +69,13 @@ export const useNavigatePage = () => { [navigate], ); + const navigateToDataflow = useCallback( + (id: string) => () => { + navigate(`${Routes.DataFlow}/${id}`); + }, + [navigate], + ); + const navigateToAgentLogs = useCallback( (id: string) => () => { navigate(`${Routes.AgentLogPage}/${id}`); @@ -86,8 +101,8 @@ export const useNavigatePage = () => { const navigateToChunkParsedResult = useCallback( (id: string, knowledgeId?: string) => () => { navigate( - // `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, `${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`, + // `${Routes.DataflowResult}?id=${knowledgeId}&doc_id=${id}&type=chunk`, ); }, [navigate], @@ -126,10 +141,16 @@ export const useNavigatePage = () => { ); const navigateToDataflowResult = useCallback( - (id: string, knowledgeId?: string) => () => { + (props: NavigateToDataflowResultProps) => () => { + let params: string[] = []; + Object.keys(props).forEach((key) => { + if (props[key]) { + params.push(`${key}=${props[key]}`); + } + }); navigate( // `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, - `${Routes.DataflowResult}/${id}`, + `${Routes.DataflowResult}?${params.join('&')}`, ); }, [navigate], @@ -155,5 +176,7 @@ export const useNavigatePage = () => { navigateToAgentList, navigateToOldProfile, navigateToDataflowResult, + navigateToDataflow, + navigateToDataFile, }; }; diff --git a/web/src/hooks/route-hook.ts b/web/src/hooks/route-hook.ts index f015eaba3..baa5b23d9 100644 --- a/web/src/hooks/route-hook.ts +++ b/web/src/hooks/route-hook.ts @@ -29,6 +29,7 @@ export const useGetKnowledgeSearchParams = () => { const [currentQueryParameters] = useSearchParams(); return { + type: currentQueryParameters.get(KnowledgeSearchParams.Type) || '', documentId: currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '', knowledgeId: diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 17d5424ba..ba6ce5d95 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -1,4 +1,5 @@ import { FileUploadProps } from '@/components/file-upload'; +import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit'; import message from '@/components/ui/message'; import { AgentGlobals } from '@/constants/agent'; import { @@ -7,6 +8,7 @@ import { IAgentLogsResponse, IFlow, IFlowTemplate, + IPipeLineListRequest, ITraceData, } from '@/interfaces/database/agent'; import { IDebugSingleRequestBody } from '@/interfaces/request/agent'; @@ -16,6 +18,7 @@ import { IInputs } from '@/pages/agent/interface'; import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks'; import agentService, { fetchAgentLogsByCanvasId, + fetchPipeLineList, fetchTrace, } from '@/services/agent-service'; import api from '@/utils/api'; @@ -31,6 +34,7 @@ import { } from './logic-hooks'; export const enum AgentApiAction { + FetchAgentListByPage = 'fetchAgentListByPage', FetchAgentList = 'fetchAgentList', UpdateAgentSetting = 'updateAgentSetting', DeleteAgent = 'deleteAgent', @@ -50,6 +54,7 @@ export const enum AgentApiAction { FetchExternalAgentInputs = 'fetchExternalAgentInputs', SetAgentSetting = 'setAgentSetting', FetchPrompt = 'fetchPrompt', + CancelDataflow = 'cancelDataflow', } export const EmptyDsl = { @@ -111,28 +116,47 @@ export const useFetchAgentListByPage = () => { const { searchString, handleInputChange } = useHandleSearchChange(); const { pagination, setPagination } = useGetPaginationWithRouter(); const debouncedSearchString = useDebounce(searchString, { wait: 500 }); + const { filterValue, handleFilterSubmit } = useHandleFilterSubmit(); + const canvasCategory = Array.isArray(filterValue.canvasCategory) + ? filterValue.canvasCategory + : []; + const owner = filterValue.owner; + + const requestParams: Record<string, any> = { + keywords: debouncedSearchString, + page_size: pagination.pageSize, + page: pagination.current, + canvas_category: + canvasCategory.length === 1 ? canvasCategory[0] : undefined, + }; + + if (Array.isArray(owner) && owner.length > 0) { + requestParams.owner_ids = owner.join(','); + } const { data, isFetching: loading } = useQuery<{ canvas: IFlow[]; total: number; }>({ queryKey: [ - AgentApiAction.FetchAgentList, + AgentApiAction.FetchAgentListByPage, { debouncedSearchString, ...pagination, + filterValue, }, ], - initialData: { canvas: [], total: 0 }, + placeholderData: (previousData) => { + if (previousData === undefined) { + return { canvas: [], total: 0 }; + } + return previousData; + }, gcTime: 0, queryFn: async () => { - const { data } = await agentService.listCanvasTeam( + const { data } = await agentService.listCanvas( { - params: { - keywords: debouncedSearchString, - page_size: pagination.pageSize, - page: pagination.current, - }, + params: requestParams, }, true, ); @@ -150,12 +174,14 @@ export const useFetchAgentListByPage = () => { ); return { - data: data.canvas, + data: data?.canvas ?? [], loading, searchString, handleInputChange: onInputChange, pagination: { ...pagination, total: data?.total }, setPagination, + filterValue, + handleFilterSubmit, }; }; @@ -173,7 +199,7 @@ export const useUpdateAgentSetting = () => { if (ret?.data?.code === 0) { message.success('success'); queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentList], + queryKey: [AgentApiAction.FetchAgentListByPage], }); } else { message.error(ret?.data?.data); @@ -197,7 +223,7 @@ export const useDeleteAgent = () => { const { data } = await agentService.removeCanvas({ canvasIds }); if (data.code === 0) { queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentList], + queryKey: [AgentApiAction.FetchAgentListByPage], }); } return data?.data ?? []; @@ -271,6 +297,7 @@ export const useSetAgent = (showMessage: boolean = true) => { title?: string; dsl?: DSL; avatar?: string; + canvas_category?: string; }) => { const { data = {} } = await agentService.setCanvas(params); if (data.code === 0) { @@ -280,7 +307,7 @@ export const useSetAgent = (showMessage: boolean = true) => { ); } queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentList], + queryKey: [AgentApiAction.FetchAgentListByPage], }); } return data; @@ -379,7 +406,7 @@ export const useUploadCanvasFileWithProgress = ( files.forEach((file) => { onError(file, error as Error); }); - message.error(error?.message); + message.error((error as Error)?.message || 'Upload failed'); } }, }); @@ -387,13 +414,11 @@ export const useUploadCanvasFileWithProgress = ( return { data, loading, uploadCanvasFile: mutateAsync }; }; -export const useFetchMessageTrace = ( - isStopFetchTrace: boolean, - canvasId?: string, -) => { +export const useFetchMessageTrace = (canvasId?: string) => { const { id } = useParams(); const queryId = id || canvasId; const [messageId, setMessageId] = useState(''); + const [isStopFetchTrace, setISStopFetchTrace] = useState(false); const { data, @@ -413,11 +438,19 @@ export const useFetchMessageTrace = ( message_id: messageId, }); - return data?.data ?? []; + return Array.isArray(data?.data) ? data?.data : []; }, }); - return { data, loading, refetch, setMessageId }; + return { + data, + loading, + refetch, + setMessageId, + messageId, + isStopFetchTrace, + setISStopFetchTrace, + }; }; export const useTestDbConnect = () => { @@ -563,7 +596,6 @@ export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => { initialData: {} as IAgentLogsResponse, gcTime: 0, queryFn: async () => { - console.log('useFetchAgentLog', searchParams); const { data } = await fetchAgentLogsByCanvasId(id as string, { ...searchParams, }); @@ -647,3 +679,59 @@ export const useFetchPrompt = () => { return { data, loading, refetch }; }; + +export const useFetchAgentList = ({ + canvas_category, +}: IPipeLineListRequest) => { + const { data, isFetching: loading } = useQuery<{ + canvas: IFlow[]; + total: number; + }>({ + queryKey: [AgentApiAction.FetchAgentList], + initialData: { canvas: [], total: 0 }, + gcTime: 0, + queryFn: async () => { + const { data } = await fetchPipeLineList({ canvas_category }); + + return data?.data ?? []; + }, + }); + + return { data, loading }; +}; + +export const useCancelDataflow = () => { + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [AgentApiAction.CancelDataflow], + mutationFn: async (taskId: string) => { + const ret = await agentService.cancelDataflow(taskId); + if (ret?.data?.code === 0) { + message.success('success'); + } else { + message.error(ret?.data?.data); + } + return ret?.data?.code; + }, + }); + + return { data, loading, cancelDataflow: mutateAsync }; +}; + +// export const useFetchKnowledgeList = () => { +// const { data, isFetching: loading } = useQuery<IFlow[]>({ +// queryKey: [AgentApiAction.FetchAgentList], +// initialData: [], +// gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3 +// queryFn: async () => { +// const { data } = await agentService.listCanvas(); + +// return data?.data ?? []; +// }, +// }); + +// return { list: data, loading }; +// }; diff --git a/web/src/hooks/use-chunk-request.ts b/web/src/hooks/use-chunk-request.ts index 320f57979..c896dd304 100644 --- a/web/src/hooks/use-chunk-request.ts +++ b/web/src/hooks/use-chunk-request.ts @@ -13,7 +13,9 @@ import { } from './logic-hooks'; import { useGetKnowledgeSearchParams } from './route-hook'; -export const useFetchNextChunkList = (): ResponseGetType<{ +export const useFetchNextChunkList = ( + enabled = true, +): ResponseGetType<{ data: IChunk[]; total: number; documentInfo: IKnowledgeFile; @@ -37,6 +39,7 @@ export const useFetchNextChunkList = (): ResponseGetType<{ placeholderData: (previousData: any) => previousData ?? { data: [], total: 0, documentInfo: {} }, // https://github.com/TanStack/query/issues/8183 gcTime: 0, + enabled, queryFn: async () => { const { data } = await kbService.chunk_list({ doc_id: documentId, diff --git a/web/src/hooks/use-dataflow-request.ts b/web/src/hooks/use-dataflow-request.ts new file mode 100644 index 000000000..c6f8a57f1 --- /dev/null +++ b/web/src/hooks/use-dataflow-request.ts @@ -0,0 +1,91 @@ +import message from '@/components/ui/message'; +import { IFlow } from '@/interfaces/database/agent'; +import dataflowService from '@/services/dataflow-service'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'umi'; + +export const enum DataflowApiAction { + ListDataflow = 'listDataflow', + RemoveDataflow = 'removeDataflow', + FetchDataflow = 'fetchDataflow', + RunDataflow = 'runDataflow', + SetDataflow = 'setDataflow', +} + +export const useRemoveDataflow = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [DataflowApiAction.RemoveDataflow], + mutationFn: async (ids: string[]) => { + const { data } = await dataflowService.removeDataflow({ + canvas_ids: ids, + }); + if (data.code === 0) { + queryClient.invalidateQueries({ + queryKey: [DataflowApiAction.ListDataflow], + }); + + message.success(t('message.deleted')); + } + return data.code; + }, + }); + + return { data, loading, removeDataflow: mutateAsync }; +}; + +export const useSetDataflow = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [DataflowApiAction.SetDataflow], + mutationFn: async (params: Partial<IFlow>) => { + const { data } = await dataflowService.setDataflow(params); + if (data.code === 0) { + queryClient.invalidateQueries({ + queryKey: [DataflowApiAction.FetchDataflow], + }); + + message.success(t(`message.${params.id ? 'modified' : 'created'}`)); + } + return data?.code; + }, + }); + + return { data, loading, setDataflow: mutateAsync }; +}; + +export const useFetchDataflow = () => { + const { id } = useParams(); + + const { + data, + isFetching: loading, + refetch, + } = useQuery<IFlow>({ + queryKey: [DataflowApiAction.FetchDataflow, id], + gcTime: 0, + initialData: {} as IFlow, + enabled: !!id, + refetchOnWindowFocus: false, + queryFn: async () => { + const { data } = await dataflowService.fetchDataflow(id); + + return data?.data ?? ({} as IFlow); + }, + }); + + return { data, loading, refetch }; +}; diff --git a/web/src/hooks/use-document-request.ts b/web/src/hooks/use-document-request.ts index bb09c4411..7e2e646d6 100644 --- a/web/src/hooks/use-document-request.ts +++ b/web/src/hooks/use-document-request.ts @@ -335,15 +335,18 @@ export const useSetDocumentParser = () => { mutationKey: [DocumentApiAction.SetDocumentParser], mutationFn: async ({ parserId, + pipelineId, documentId, parserConfig, }: { parserId: string; + pipelineId: string; documentId: string; parserConfig: IChangeParserConfigRequestBody; }) => { const { data } = await kbService.document_change_parser({ parser_id: parserId, + pipeline_id: pipelineId, doc_id: documentId, parser_config: parserConfig, }); diff --git a/web/src/hooks/use-knowledge-request.ts b/web/src/hooks/use-knowledge-request.ts index 8807cd5d9..d99167338 100644 --- a/web/src/hooks/use-knowledge-request.ts +++ b/web/src/hooks/use-knowledge-request.ts @@ -31,6 +31,7 @@ export const enum KnowledgeApiAction { FetchKnowledgeDetail = 'fetchKnowledgeDetail', FetchKnowledgeGraph = 'fetchKnowledgeGraph', FetchMetadata = 'fetchMetadata', + FetchKnowledgeList = 'fetchKnowledgeList', RemoveKnowledgeGraph = 'removeKnowledgeGraph', } @@ -238,7 +239,11 @@ export const useUpdateKnowledge = (shouldFetchList = false) => { return { data, loading, saveKnowledgeConfiguration: mutateAsync }; }; -export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { +export const useFetchKnowledgeBaseConfiguration = (props?: { + isEdit?: boolean; + refreshCount?: number; +}) => { + const { isEdit = true, refreshCount } = props || { isEdit: true }; const { id } = useParams(); const [searchParams] = useSearchParams(); const knowledgeBaseId = searchParams.get('id') || id; @@ -255,10 +260,14 @@ export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { initialData: {} as IKnowledge, gcTime: 0, queryFn: async () => { - const { data } = await kbService.get_kb_detail({ - kb_id: knowledgeBaseId, - }); - return data?.data ?? {}; + if (isEdit) { + const { data } = await kbService.get_kb_detail({ + kb_id: knowledgeBaseId, + }); + return data?.data ?? {}; + } else { + return {}; + } }, }); @@ -323,3 +332,25 @@ export const useRemoveKnowledgeGraph = () => { return { data, loading, removeKnowledgeGraph: mutateAsync }; }; + +export const useFetchKnowledgeList = ( + shouldFilterListWithoutDocument: boolean = false, +): { + list: IKnowledge[]; + loading: boolean; +} => { + const { data, isFetching: loading } = useQuery({ + queryKey: [KnowledgeApiAction.FetchKnowledgeList], + initialData: [], + gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3 + queryFn: async () => { + const { data } = await listDataset(); + const list = data?.data?.kbs ?? []; + return shouldFilterListWithoutDocument + ? list.filter((x: IKnowledge) => x.chunk_num > 0) + : list; + }, + }); + + return { list: data, loading }; +}; diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index 7994426ea..8bd33e89b 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -30,6 +30,7 @@ export interface ISwitchForm { no: string; } +import { AgentCategory } from '@/constants/agent'; import { Edge, Node } from '@xyflow/react'; import { IReference, Message } from './chat'; @@ -74,6 +75,7 @@ export declare interface IFlow { permission: string; nickname: string; operator_permission: number; + canvas_category: string; } export interface IFlowTemplate { @@ -265,3 +267,12 @@ export interface IAgentLogMessage { role: 'user' | 'assistant'; id: string; } + +export interface IPipeLineListRequest { + page?: number; + page_size?: number; + keywords?: string; + orderby?: string; + desc?: boolean; + canvas_category?: AgentCategory; +} diff --git a/web/src/interfaces/database/document.ts b/web/src/interfaces/database/document.ts index dfc284835..5edbad160 100644 --- a/web/src/interfaces/database/document.ts +++ b/web/src/interfaces/database/document.ts @@ -5,12 +5,15 @@ export interface IDocumentInfo { create_date: string; create_time: number; created_by: string; + nickname: string; id: string; kb_id: string; location: string; name: string; parser_config: IParserConfig; parser_id: string; + pipeline_id: string; + pipeline_name: string; process_begin_at?: string; process_duration: number; progress: number; @@ -19,6 +22,7 @@ export interface IDocumentInfo { size: number; source_type: string; status: string; + suffix: string; thumbnail: string; token_num: number; type: string; diff --git a/web/src/interfaces/database/knowledge.ts b/web/src/interfaces/database/knowledge.ts index 0d8fc025e..d768d8e5e 100644 --- a/web/src/interfaces/database/knowledge.ts +++ b/web/src/interfaces/database/knowledge.ts @@ -14,6 +14,9 @@ export interface IKnowledge { name: string; parser_config: ParserConfig; parser_id: string; + pipeline_id: string; + pipeline_name: string; + pipeline_avatar: string; permission: string; similarity_threshold: number; status: string; @@ -26,6 +29,10 @@ export interface IKnowledge { nickname: string; operator_permission: number; size: number; + raptor_task_finish_at?: string; + raptor_task_id?: string; + mindmap_task_finish_at?: string; + mindmap_task_id?: string; } export interface IKnowledgeResult { diff --git a/web/src/interfaces/request/document.ts b/web/src/interfaces/request/document.ts index 6097da28b..88bb4496f 100644 --- a/web/src/interfaces/request/document.ts +++ b/web/src/interfaces/request/document.ts @@ -7,6 +7,7 @@ export interface IChangeParserConfigRequestBody { export interface IChangeParserRequestBody { parser_id: string; + pipeline_id: string; doc_id: string; parser_config: IChangeParserConfigRequestBody; } diff --git a/web/src/layouts/next-header.tsx b/web/src/layouts/next-header.tsx index f7d7c9fb3..26cce2ba0 100644 --- a/web/src/layouts/next-header.tsx +++ b/web/src/layouts/next-header.tsx @@ -1,3 +1,4 @@ +import { IconFontFill } from '@/components/icon-font'; import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { useTheme } from '@/components/theme-provider'; import { Button } from '@/components/ui/button'; @@ -20,7 +21,6 @@ import { CircleHelp, Cpu, File, - Github, House, Library, MessageSquareText, @@ -114,15 +114,6 @@ export function Header() { className="size-10 mr-[12] cursor-pointer" onClick={handleLogoClick} /> - <a - className="flex items-center gap-1.5 text-text-secondary" - target="_blank" - href="https://github.com/infiniflow/ragflow" - rel="noreferrer" - > - <Github className="size-4" /> - {/* <span className=" text-base">21.5k stars</span> */} - </a> </div> <Segmented options={options} @@ -130,6 +121,20 @@ export function Header() { onChange={handleChange} ></Segmented> <div className="flex items-center gap-5 text-text-badge"> + <a + target="_blank" + href="https://discord.com/invite/NjYzJD3GM3" + rel="noreferrer" + > + <IconFontFill name="a-DiscordIconSVGVectorIcon"></IconFontFill> + </a> + <a + target="_blank" + href="https://github.com/infiniflow/ragflow" + rel="noreferrer" + > + <IconFontFill name="GitHub"></IconFontFill> + </a> <DropdownMenu> <DropdownMenuTrigger> <div className="flex items-center gap-1"> diff --git a/web/src/less/mixins.less b/web/src/less/mixins.less index 119c1a089..27049deea 100644 --- a/web/src/less/mixins.less +++ b/web/src/less/mixins.less @@ -5,7 +5,7 @@ .chunkText() { em { - color: red; + color: var(--accent-primary); font-style: normal; } table { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 192afe284..ef9dc4095 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -102,16 +102,24 @@ export default { noMoreData: `That's all. Nothing more.`, }, knowledgeDetails: { + fileSize: 'File Size', + fileType: 'File Type', + uploadedBy: 'Uploaded by', + notGenerated: 'Not generated', + generatedOn: 'Generated on', + subbarFiles: 'Files', generateKnowledgeGraph: 'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.', generateRaptor: 'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.', generate: 'Generate', raptor: 'Raptor', - knowledgeGraph: 'Knowledge Graph', processingType: 'Processing Type', dataPipeline: 'Data Pipeline', operations: 'Operations', + taskId: 'Task ID', + duration: 'Duration', + details: 'Details', status: 'Status', task: 'Task', startDate: 'Start Date', @@ -123,7 +131,7 @@ export default { success: 'Success', failed: 'Failed', completed: 'Completed', - processLog: 'Process Log', + datasetLog: 'Dataset Log', created: 'Created', learnMore: 'Learn More', general: 'General', @@ -138,12 +146,12 @@ export default { testing: 'Retrieval testing', files: 'files', configuration: 'Configuration', - knowledgeGraph: 'Knowledge graph', + knowledgeGraph: 'Knowledge Graph', name: 'Name', namePlaceholder: 'Please input name!', doc: 'Docs', datasetDescription: - '😉 Please wait for your files to finish parsing before starting an AI-powered chat.', + 'Please wait for your files to finish parsing before starting an AI-powered chat.', addFile: 'Add file', searchFiles: 'Search your files', localFiles: 'Local files', @@ -261,13 +269,30 @@ export default { reRankModelWaring: 'Re-rank model is very time consuming.', }, knowledgeConfiguration: { + deleteGenerateModalContent: ` + <p>Deleting the generated <strong class='text-text-primary'>{{type}}</strong> results + will remove all derived entities and relationships from this dataset. + Your original files will remain intact.<p> + <br/> + Do you want to continue? + `, + extractRaptor: 'Extract Raptor', + extractKnowledgeGraph: 'Extract Knowledge Graph', + filterPlaceholder: 'please input filter', + fileFilterTip: '', + fileFilter: 'File Filter', + setDefaultTip: '', + setDefault: 'Set as Default', + eidtLinkDataPipeline: 'Edit Data Pipeline', + linkPipelineSetTip: 'Manage data pipeline linkage with this dataset', + default: 'Default', + dataPipeline: 'Data Pipeline', + linkDataPipeline: 'Link Data Pipeline', enableAutoGenerate: 'Enable Auto Generate', teamPlaceholder: 'Please select a team.', - dataFlowPlaceholder: 'Please select a data flow.', + dataFlowPlaceholder: 'Please select a pipeline.', buildItFromScratch: 'Build it from scratch', - useRAPTORToEnhanceRetrieval: 'Use RAPTOR to Enhance Retrieval', - extractKnowledgeGraph: 'Extract Knowledge Graph', - dataFlow: 'Data Flow', + dataFlow: 'Pipeline', parseType: 'Parse Type', manualSetup: 'Manual Setup', builtIn: 'Built-in', @@ -395,7 +420,7 @@ export default { <p>In a Tag column, <b>comma</b> is used to separate tags.</p> <i>Lines of texts that fail to follow the above rules will be ignored.</i> `, - useRaptor: 'Use RAPTOR to enhance retrieval', + useRaptor: 'RAPTOR', useRaptorTip: 'Enable RAPTOR for multi-hop question-answering tasks. See https://ragflow.io/docs/dev/enable_raptor for details.', prompt: 'Prompt', @@ -441,7 +466,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s topnTags: 'Top-N Tags', tags: 'Tags', addTag: 'Add tag', - useGraphRag: 'Extract knowledge graph', + useGraphRag: 'Knowledge graph', useGraphRagTip: 'Construct a knowledge graph over file chunks of the current knowledge base to enhance multi-hop question-answering involving nested logic. See https://ragflow.io/docs/dev/construct_knowledge_graph for details.', graphRagMethod: 'Method', @@ -449,7 +474,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s General: Use prompts provided by github.com/microsoft/graphrag to extract entities and relationships`, resolution: 'Entity resolution', resolutionTip: `An entity deduplication switch. When enabled, the LLM will combine similar entities - e.g., '2025' and 'the year of 2025', or 'IT' and 'Information Technology' - to construct a more accurate graph`, - community: 'Community reports generation', + community: 'Community reports', communityTip: 'In a knowledge graph, a community is a cluster of entities linked by relationships. You can have the LLM generate an abstract for each community, known as a community report. See here for more information: https://www.microsoft.com/en-us/research/blog/graphrag-improving-global-search-via-dynamic-community-selection/', theDocumentBeingParsedCannotBeDeleted: @@ -1040,7 +1065,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s {input} The above is the content you need to summarize.`, createGraph: 'Create agent', - createFromTemplates: 'Create from templates', + createFromTemplates: 'Create from template', retrieval: 'Retrieval', generate: 'Generate', answer: 'Interact', @@ -1560,6 +1585,13 @@ This delimiter is used to split the input text into several text pieces echo of sqlStatementTip: 'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.', frameworkPrompts: 'Framework', + release: 'Publish', + createFromBlank: 'Create from blank', + createFromTemplate: 'Create from template', + importJsonFile: 'Import JSON file', + ceateAgent: 'Agent flow', + createPipeline: 'Data pipeline', + chooseAgentType: 'Choose Agent Type', }, llmTools: { bad_calculator: { @@ -1583,6 +1615,9 @@ This delimiter is used to split the input text into several text pieces echo of serverType: 'Server Type', addMCP: 'Add MCP', editMCP: 'Edit MCP', + toolsAvailable: 'tools available', + mcpServers: 'MCP Servers', + customizeTheListOfMcpServers: 'Customize the list of MCP servers', }, search: { searchApps: 'Search Apps', @@ -1630,14 +1665,138 @@ This delimiter is used to split the input text into several text pieces echo of parseSummaryTip: 'Parser:deepdoc', rerunFromCurrentStep: 'Rerun From Current Step', rerunFromCurrentStepTip: 'Changes detected. Click to re-run.', + confirmRerun: 'Confirm Rerun Process', + confirmRerunModalContent: ` + <p class="text-sm text-text-disabled font-medium mb-2"> + You are about to rerun the process starting from the <strong class="text-text-primary">{{step}}</strong> step. + </p> + <p class="text-sm mb-3 text-text-secondary">This will:</p> + <ul class="list-disc list-inside space-y-1 text-sm text-text-secondary"> + <li>Overwrite existing results from the current step onwards</li> + <li>Create a new log entry for tracking</li> + <li>Previous steps will remain unchanged</li> + </ul>`, + changeStepModalTitle: 'Step Switch Warning', + changeStepModalContent: ` + <p>You are currently editing the results of this stage.</p> + <p>If you switch to a later stage, your changes will be lost. </p> + <p>To keep them, please click Rerun to re-run the current stage.</p> `, + changeStepModalConfirmText: 'Switch Anyway', + changeStepModalCancelText: 'Cancel', + unlinkPipelineModalTitle: 'Unlink data pipeline', + unlinkPipelineModalContent: ` + <p>Once unlinked, this Dataset will no longer be connected to the current Data Pipeline.</p> + <p>Files that are already being parsed will continue until completion</p> + <p>Files that are not yet parsed will no longer be processed</p> <br/> + <p>Are you sure you want to proceed?</p> `, + unlinkPipelineModalConfirmText: 'Unlink', }, dataflow: { parser: 'Parser', - parserDescription: 'Parser', - chunker: 'Chunker', - chunkerDescription: 'Chunker', + parserDescription: + 'Extracts raw text and structure from files for downstream processing.', tokenizer: 'Tokenizer', - tokenizerDescription: 'Tokenizer', + tokenizerDescription: + 'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.', + splitter: 'Token Splitter', + splitterDescription: + 'Split text into chunks by token length with optional delimiters and overlap.', + hierarchicalMergerDescription: + 'Split documents into sections by title hierarchy with regex rules for finer control.', + hierarchicalMerger: 'Title Splitter', + extractor: 'Context Generator', + extractorDescription: + 'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.', + outputFormat: 'Output format', + lang: 'Language', + fileFormats: 'File formats', + fields: 'Fields', + addParser: 'Add Parser', + hierarchy: 'Hierarchy', + regularExpressions: 'Regular Expressions', + overlappedPercent: 'Overlapped percent', + searchMethod: 'Search method', + begin: 'File', + parserMethod: 'Parser method', + systemPrompt: 'System Prompt', + systemPromptPlaceholder: + 'Enter system prompt for image analysis, if empty the system default value will be used', + exportJson: 'Export JSON', + viewResult: 'View Result', + running: 'Running', + summary: 'Augmented Context', + keywords: 'Keywords', + questions: 'Questions', + metadata: 'Metadata', + fieldName: 'Result Destination', + prompts: { + system: { + keywords: `Role +You are a text analyzer. + +Task +Extract the most important keywords/phrases of a given piece of text content. + +Requirements +- Summarize the text content, and give the top 5 important keywords/phrases. +- The keywords MUST be in the same language as the given piece of text content. +- The keywords are delimited by ENGLISH COMMA. +- Output keywords ONLY.`, + questions: `Role +You are a text analyzer. + +Task +Propose 3 questions about a given piece of text content. + +Requirements +- Understand and summarize the text content, and propose the top 3 important questions. +- The questions SHOULD NOT have overlapping meanings. +- The questions SHOULD cover the main content of the text as much as possible. +- The questions MUST be in the same language as the given piece of text content. +- One question per line. +- Output questions ONLY.`, + summary: `Act as a precise summarizer. Your task is to create a summary of the provided content that is both concise and faithful to the original. + +Key Instructions: +1. Accuracy: Strictly base the summary on the information given. Do not introduce any new facts, conclusions, or interpretations that are not explicitly stated. +2. Language: Write the summary in the same language as the source text. +3. Objectivity: Present the key points without bias, preserving the original intent and tone of the content. Do not editorialize. +4. Conciseness: Focus on the most important ideas, omitting minor details and fluff.`, + metadata: `Extract important structured information from the given content. Output ONLY a valid JSON string with no additional text. If no important structured information is found, output an empty JSON object: {}. + +Important structured information may include: names, dates, locations, events, key facts, numerical data, or other extractable entities.`, + }, + user: { + keywords: `Text Content +[Insert text here]`, + questions: `Text Content +[Insert text here]`, + summary: `Text to Summarize: +[Insert text here]`, + metadata: `Content: [INSERT CONTENT HERE]`, + }, + }, + cancel: 'Cancel', + swicthPromptMessage: + 'The prompt word will change. Please confirm whether to abandon the existing prompt word?', + tokenizerSearchMethodOptions: { + full_text: 'Full-text', + embedding: 'Embedding', + }, + filenameEmbeddingWeight: 'Filename embedding weight', + tokenizerFieldsOptions: { + text: 'Text', + keywords: 'Keywords', + questions: 'Questions', + summary: 'Augmented Context', + }, + }, + datasetOverview: { + downloadTip: 'Files being downloaded from data sources. ', + processingTip: 'Files being processed by data flows.', + totalFiles: 'Total Files', + downloading: 'Downloading', + processing: 'Processing', }, }, }; diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index c4c79d89c..7d79476ae 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -94,12 +94,20 @@ export default { noMoreData: '没有更多数据了', }, knowledgeDetails: { + fileSize: '文件大小', + fileType: '文件类型', + uploadedBy: '创建者', + notGenerated: '未生成', + generatedOn: '生成于', + subbarFiles: '文件列表', generate: '生成', raptor: 'Raptor', - knowledgeGraph: '知识图谱', processingType: '处理类型', dataPipeline: '数据管道', operations: '操作', + taskId: '任务ID', + duration: '耗时', + details: '详情', status: '状态', task: '任务', startDate: '开始时间', @@ -111,7 +119,7 @@ export default { success: '成功', failed: '失败', completed: '已完成', - processLog: '处理进度日志', + datasetLog: '知识库日志', created: '创建于', learnMore: '了解更多', general: '通用', @@ -130,7 +138,7 @@ export default { name: '名称', namePlaceholder: '请输入名称', doc: '文档', - datasetDescription: '😉 解析成功后才能问答哦。', + datasetDescription: '解析成功后才能问答哦。', addFile: '新增文件', searchFiles: '搜索文件', localFiles: '本地文件', @@ -246,12 +254,29 @@ export default { theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除', }, knowledgeConfiguration: { + deleteGenerateModalContent: ` + <p>删除生成的 <strong class='text-text-primary'>{{type}}</strong> 结果 + 将从此数据集中移除所有派生实体和关系。 + 您的原始文件将保持不变。<p> + <br/> + 是否要继续? + `, + extractRaptor: '从文档中提取Raptor', + extractKnowledgeGraph: '从文档中提取知识图谱', + filterPlaceholder: '请输入', + fileFilterTip: '', + fileFilter: '正则匹配表达式', + setDefaultTip: '', + setDefault: '设置默认', + eidtLinkDataPipeline: '编辑数据流', + linkPipelineSetTip: '管理与此数据集的数据管道链接', + default: '默认', + dataPipeline: '数据流', + linkDataPipeline: '关联数据流', enableAutoGenerate: '是否启用自动生成', teamPlaceholder: '请选择团队', dataFlowPlaceholder: '请选择数据流', buildItFromScratch: '去Scratch构建', - useRAPTORToEnhanceRetrieval: '使用 RAPTOR 提升检索效果', - extractKnowledgeGraph: '知识图谱提取', dataFlow: '数据流', parseType: '切片方法', manualSetup: '手动设置', @@ -1471,6 +1496,11 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 sqlStatementTip: '在此处编写您的 SQL 查询。您可以使用变量、原始 SQL,或使用变量语法混合使用两者。', frameworkPrompts: '框架', + release: '发布', + createFromBlank: '从空白创建', + createFromTemplate: '从模板创建', + importJsonFile: '导入 JSON 文件', + chooseAgentType: '选择智能体类型', }, footer: { profile: 'All rights reserved @ React', @@ -1494,6 +1524,17 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 okText: '确认', cancelText: '取消', }, + mcp: { + export: '导出', + import: '导入', + url: 'URL', + serverType: '服务器类型', + addMCP: '添加 MCP', + editMCP: '编辑 MCP', + toolsAvailable: '可用的工具', + mcpServers: 'MCP 服务器', + customizeTheListOfMcpServers: '自定义 MCP 服务器列表', + }, search: { searchApps: '搜索', createSearch: '创建查询', @@ -1540,14 +1581,127 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 parseSummaryTip: '解析器: deepdoc', rerunFromCurrentStep: '从当前步骤重新运行', rerunFromCurrentStepTip: '已修改,点击重新运行。', + confirmRerun: '确认重新运行流程', + confirmRerunModalContent: ` + <p class="text-sm text-text-disabled font-medium mb-2"> + 您即将从 <strong class="text-text-primary">{{step}}</strong> 步骤开始重新运行该过程 + </p> + <p class="text-sm mb-3 text-text-secondary">这将:</p> + <ul class="list-disc list-inside space-y-1 text-sm text-text-secondary"> + <li>从当前步骤开始覆盖现有结果</li> + <li>创建新的日志条目进行跟踪</li> + <li>之前的步骤将保持不变</li> + </ul>`, + changeStepModalTitle: '切换步骤警告', + changeStepModalContent: ` + <p>您目前正在编辑此阶段的结果。</p> + <p>如果您切换到后续阶段,您的更改将会丢失。</p> + <p>要保留这些更改,请点击“重新运行”以重新运行当前阶段。</p> `, + changeStepModalConfirmText: '继续切换', + changeStepModalCancelText: '取消', + unlinkPipelineModalTitle: '解绑数据流', + unlinkPipelineModalContent: ` + <p>一旦取消链接,该数据集将不再连接到当前数据管道。</p> + <p>正在解析的文件将继续解析,直到完成。</p> + <p>尚未解析的文件将不再被处理。</p> <br/> + <p>你确定要继续吗?</p> `, + unlinkPipelineModalConfirmText: '解绑', }, dataflow: { parser: '解析器', - parserDescription: '解析器', - chunker: '分块器', - chunkerDescription: '分块器', + parserDescription: '从文件中提取原始文本和结构以供下游处理。', tokenizer: '分词器', - tokenizerDescription: '分词器', + tokenizerDescription: + '根据所选的搜索方法,将文本转换为所需的数据结构(例如,用于嵌入搜索的向量嵌入)。', + splitter: '分词器拆分器', + splitterDescription: + '根据分词器长度将文本拆分成块,并带有可选的分隔符和重叠。', + hierarchicalMergerDescription: + '使用正则表达式规则按标题层次结构将文档拆分成多个部分,以实现更精细的控制。', + hierarchicalMerger: '标题拆分器', + extractor: '提取器', + extractorDescription: + '使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。', + outputFormat: '输出格式', + lang: '语言', + fileFormats: '文件格式', + fields: '字段', + addParser: '增加解析器', + hierarchy: '层次结构', + regularExpressions: '正则表达式', + overlappedPercent: '重叠百分比', + searchMethod: '搜索方法', + filenameEmbdWeight: '文件名嵌入权重', + begin: '文件', + parserMethod: '解析方法', + systemPrompt: '系统提示词', + systemPromptPlaceholder: + '请输入用于图像分析的系统提示词,若为空则使用系统缺省值', + exportJson: '导出 JSON', + viewResult: '查看结果', + running: '运行中', + summary: '增强上下文', + keywords: '关键词', + questions: '问题', + metadata: '元数据', + fieldName: '结果目的地', + prompts: { + system: { + keywords: `角色 +你是一名文本分析员。 + +任务 +从给定的文本内容中提取最重要的关键词/短语。 + +要求 +- 总结文本内容,并给出最重要的5个关键词/短语。 +- 关键词必须与给定的文本内容使用相同的语言。 +- 关键词之间用英文逗号分隔。 +- 仅输出关键词。`, + questions: `角色 +你是一名文本分析员。 + +任务 +针对给定的文本内容提出3个问题。 + +要求 +- 理解并总结文本内容,并提出最重要的3个问题。 +- 问题的含义不应重叠。 +- 问题应尽可能涵盖文本的主要内容。 +- 问题必须与给定的文本内容使用相同的语言。 +- 每行一个问题。 +- 仅输出问题。`, + summary: `扮演一个精准的摘要者。你的任务是为提供的内容创建一个简洁且忠实于原文的摘要。 + +关键说明: +1. 准确性:摘要必须严格基于所提供的信息。请勿引入任何未明确说明的新事实、结论或解释。 +2. 语言:摘要必须使用与原文相同的语言。 +3. 客观性:不带偏见地呈现要点,保留内容的原始意图和语气。请勿进行编辑。 +4. 简洁性:专注于最重要的思想,省略细节和多余的内容。`, + metadata: `从给定内容中提取重要的结构化信息。仅输出有效的 JSON 字符串,不包含任何附加文本。如果未找到重要的结构化信息,则输出一个空的 JSON 对象:{}。 + +重要的结构化信息可能包括:姓名、日期、地点、事件、关键事实、数字数据或其他可提取实体。`, + }, + user: { + keywords: `文本内容 +[在此处插入文本]`, + questions: `文本内容 +[在此处插入文本]`, + summary: `要总结的文本: +[在此处插入文本]`, + metadata: `内容:[在此处插入内容]`, + }, + }, + cancel: '取消', + filenameEmbeddingWeight: '文件名嵌入权重', + switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?', + }, + datasetOverview: { + downloadTip: '正在从数据源下载文件。', + processingTip: '正在由数据流处理文件。', + totalFiles: '文件总数', + downloading: '正在下载', + processing: '正在处理', }, }, }; diff --git a/web/src/pages/agent/canvas/edge/index.tsx b/web/src/pages/agent/canvas/edge/index.tsx index b82f4617e..5a3385c01 100644 --- a/web/src/pages/agent/canvas/edge/index.tsx +++ b/web/src/pages/agent/canvas/edge/index.tsx @@ -43,7 +43,9 @@ function InnerButtonEdge({ targetPosition, }); const selectedStyle = useMemo(() => { - return selected ? { strokeWidth: 1, stroke: 'var(--accent-primary)' } : {}; + return selected + ? { strokeWidth: 1, stroke: 'rgb(var(--accent-primary))' } + : {}; }, [selected]); const placeholderHighlightStyle = useMemo(() => { @@ -67,7 +69,7 @@ function InnerButtonEdge({ let index = idx - 1; while (index >= 0) { if (path[index] === source) { - return { strokeWidth: 1, stroke: 'var(--accent-primary)' }; + return { strokeWidth: 1, stroke: 'rgb(var(--accent-primary))' }; } index--; } diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index 3136f6777..b40b42e17 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -7,6 +7,7 @@ import { AgentGlobalsSysQueryWithBrace, CodeTemplateStrMap, ProgrammingLanguage, + initialLlmBaseValues, } from '@/constants/agent'; export enum AgentDialogueMode { @@ -14,13 +15,8 @@ export enum AgentDialogueMode { Task = 'task', } -import { - ChatVariableEnabledField, - variableEnabledFieldMap, -} from '@/constants/chat'; import { ModelVariableType } from '@/constants/knowledge'; import i18n from '@/locales/config'; -import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat'; import { t } from 'i18next'; // DuckDuckGo's channel options @@ -276,24 +272,6 @@ export const initialBeginValues = { prologue: `Hi! I'm your assistant. What can I do for you?`, }; -export const variableCheckBoxFieldMap = Object.keys( - variableEnabledFieldMap, -).reduce<Record<string, boolean>>((pre, cur) => { - pre[cur] = setInitialChatVariableEnabledFieldValue( - cur as ChatVariableEnabledField, - ); - return pre; -}, {}); - -const initialLlmBaseValues = { - ...variableCheckBoxFieldMap, - temperature: 0.1, - top_p: 0.3, - frequency_penalty: 0.7, - presence_penalty: 0.4, - max_tokens: 256, -}; - export const initialGenerateValues = { ...initialLlmBaseValues, prompt: i18n.t('flow.promptText'), diff --git a/web/src/pages/agent/debug-content/uploader.tsx b/web/src/pages/agent/debug-content/uploader.tsx index e11a6f41d..88465d15c 100644 --- a/web/src/pages/agent/debug-content/uploader.tsx +++ b/web/src/pages/agent/debug-content/uploader.tsx @@ -86,7 +86,7 @@ export function FileUploadDirectUpload({ </div> <p className="font-medium text-sm">Drag & drop files here</p> <p className="text-muted-foreground text-xs"> - Or click to browse (max 2 files) + Or click to browse (max 1 files) </p> </div> <FileUploadTrigger asChild> diff --git a/web/src/pages/agent/form/components/prompt-editor/index.tsx b/web/src/pages/agent/form/components/prompt-editor/index.tsx index ffab9b44e..c0242293a 100644 --- a/web/src/pages/agent/form/components/prompt-editor/index.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/index.tsx @@ -55,7 +55,7 @@ type IProps = { onChange?: (value?: string) => void; placeholder?: ReactNode; } & PromptContentProps & - Pick<VariablePickerMenuPluginProps, 'extraOptions'>; + Pick<VariablePickerMenuPluginProps, 'extraOptions' | 'baseOptions'>; function PromptContent({ showToolbar = true, @@ -126,6 +126,7 @@ export function PromptEditor({ showToolbar, multiLine = true, extraOptions, + baseOptions, }: IProps) { const { t } = useTranslation(); const initialConfig: InitialConfigType = { @@ -177,6 +178,7 @@ export function PromptEditor({ <VariablePickerMenuPlugin value={value} extraOptions={extraOptions} + baseOptions={baseOptions} ></VariablePickerMenuPlugin> <PasteHandlerPlugin /> <VariableOnChangePlugin diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx index 7488d991a..cb4905b45 100644 --- a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx @@ -10,7 +10,6 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext import { LexicalTypeaheadMenuPlugin, MenuOption, - useBasicTypeaheadTriggerMatch, } from '@lexical/react/LexicalTypeaheadMenuPlugin'; import { $createParagraphNode, @@ -109,29 +108,56 @@ function VariablePickerMenuItem({ ); } +export type VariablePickerMenuOptionType = { + label: string; + title: string; + value?: string; + options: Array<{ + label: string; + value: string; + icon: ReactNode; + }>; +}; + export type VariablePickerMenuPluginProps = { value?: string; - extraOptions?: Array<{ - label: string; - title: string; - options: Array<{ label: string; value: string; icon?: ReactNode }>; - }>; + extraOptions?: VariablePickerMenuOptionType[]; + baseOptions?: VariablePickerMenuOptionType[]; }; export default function VariablePickerMenuPlugin({ value, extraOptions, + baseOptions, }: VariablePickerMenuPluginProps): JSX.Element { const [editor] = useLexicalComposerContext(); - const isFirstRender = useRef(true); - const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { - minLength: 0, - }); + // const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { + // minLength: 0, + // }); + + const testTriggerFn = React.useCallback((text: string) => { + const lastChar = text.slice(-1); + if (lastChar === '/') { + console.log('Found trigger character "/"'); + return { + leadOffset: text.length - 1, + matchingString: '', + replaceableString: '/', + }; + } + return null; + }, []); + + const previousValue = useRef<string | undefined>(); const [queryString, setQueryString] = React.useState<string | null>(''); let options = useBuildQueryVariableOptions(); + if (baseOptions) { + options = baseOptions as typeof options; + } + const buildNextOptions = useCallback(() => { let filteredOptions = [...options, ...(extraOptions ?? [])]; if (queryString) { @@ -267,8 +293,8 @@ export default function VariablePickerMenuPlugin({ ); useEffect(() => { - if (editor && value && isFirstRender.current) { - isFirstRender.current = false; + if (editor && value && value !== previousValue.current) { + previousValue.current = value; editor.update( () => { parseTextToVariableNodes(value); @@ -278,6 +304,21 @@ export default function VariablePickerMenuPlugin({ } }, [parseTextToVariableNodes, editor, value]); + // Fixed the issue where the cursor would go to the end when changing its own data + useEffect(() => { + return editor.registerUpdateListener(({ editorState, tags }) => { + // If we trigger the programmatic update ourselves, we should not write back to avoid an infinite loop. + if (tags.has(ProgrammaticTag)) return; + + editorState.read(() => { + const text = $getRoot().getTextContent(); + if (text !== previousValue.current) { + previousValue.current = text; + } + }); + }); + }, [editor]); + return ( <LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption> onQueryChange={setQueryString} @@ -288,7 +329,7 @@ export default function VariablePickerMenuPlugin({ closeMenu, ) } - triggerFn={checkForTriggerMatch} + triggerFn={testTriggerFn} options={buildNextOptions()} menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { const nextOptions = buildNextOptions(); diff --git a/web/src/pages/agent/form/iteration-form/use-build-options.ts b/web/src/pages/agent/form/iteration-form/use-build-options.ts index 3439000d4..46fa7ee30 100644 --- a/web/src/pages/agent/form/iteration-form/use-build-options.ts +++ b/web/src/pages/agent/form/iteration-form/use-build-options.ts @@ -1,7 +1,7 @@ +import { buildOutputOptions } from '@/utils/canvas-util'; 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) { diff --git a/web/src/pages/agent/hooks/use-get-begin-query.tsx b/web/src/pages/agent/hooks/use-get-begin-query.tsx index 83cda2078..10fd4361d 100644 --- a/web/src/pages/agent/hooks/use-get-begin-query.tsx +++ b/web/src/pages/agent/hooks/use-get-begin-query.tsx @@ -1,19 +1,11 @@ import { AgentGlobals } from '@/constants/agent'; import { useFetchAgent } from '@/hooks/use-agent-request'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { Edge } from '@xyflow/react'; +import { buildNodeOutputOptions } from '@/utils/canvas-util'; 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 { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { AgentDialogueMode, BeginId, @@ -83,72 +75,18 @@ export const useGetBeginNodeDataQueryIsSafe = () => { return isBeginNodeDataQuerySafe; }; -function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) { - return nodeIds.reduce<string[]>((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<string, any> = {}, - 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, - <OperatorIcon name={x.data.label as Operator} />, - ), - })); + return useMemo(() => { + return buildNodeOutputOptions({ + nodes, + edges, + nodeId, + Icon: ({ name }) => <OperatorIcon name={name as Operator}></OperatorIcon>, + }); }, [edges, nodeId, nodes]); - - return nodeOutputOptions; } // exclude nodes with branches diff --git a/web/src/pages/agent/log-sheet/workflow-timeline.tsx b/web/src/pages/agent/log-sheet/workflow-timeline.tsx index 8e4024228..8b9eb5ef7 100644 --- a/web/src/pages/agent/log-sheet/workflow-timeline.tsx +++ b/web/src/pages/agent/log-sheet/workflow-timeline.tsx @@ -23,7 +23,7 @@ import { ITraceData } from '@/interfaces/database/agent'; import { cn } from '@/lib/utils'; import { t } from 'i18next'; import { get, isEmpty, isEqual, uniqWith } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import JsonView from 'react18-json-view'; import { Operator } from '../constant'; import { useCacheChatLog } from '../hooks/use-cache-chat-log'; @@ -116,12 +116,12 @@ export const WorkFlowTimeline = ({ isShare, }: LogFlowTimelineProps) => { // const getNode = useGraphStore((state) => state.getNode); - const [isStopFetchTrace, setISStopFetchTrace] = useState(false); - const { data: traceData, setMessageId } = useFetchMessageTrace( - isStopFetchTrace, - canvasId, - ); + const { + data: traceData, + setMessageId, + setISStopFetchTrace, + } = useFetchMessageTrace(canvasId); useEffect(() => { setMessageId(currentMessageId); @@ -133,7 +133,7 @@ export const WorkFlowTimeline = ({ useEffect(() => { setISStopFetchTrace(!sendLoading); - }, [sendLoading]); + }, [sendLoading, setISStopFetchTrace]); const startedNodeList = useMemo(() => { const finish = currentEventListWithoutMessage?.some( @@ -151,7 +151,7 @@ export const WorkFlowTimeline = ({ } return pre; }, []); - }, [currentEventListWithoutMessage, sendLoading]); + }, [currentEventListWithoutMessage, sendLoading, setISStopFetchTrace]); const getElapsedTime = (nodeId: string) => { if (nodeId === 'begin') { diff --git a/web/src/pages/agent/run-sheet/index.tsx b/web/src/pages/agent/run-sheet/index.tsx index cac62d008..d7a339b90 100644 --- a/web/src/pages/agent/run-sheet/index.tsx +++ b/web/src/pages/agent/run-sheet/index.tsx @@ -51,7 +51,7 @@ const RunSheet = ({ ); return ( - <Sheet onOpenChange={hideModal} open> + <Sheet onOpenChange={hideModal} open modal={false}> <SheetContent className={cn('top-20 p-2')}> <SheetHeader> <SheetTitle>{t('flow.testRun')}</SheetTitle> diff --git a/web/src/pages/agents/agent-card.tsx b/web/src/pages/agents/agent-card.tsx index e1aa20ac4..2ae1ab0c8 100644 --- a/web/src/pages/agents/agent-card.tsx +++ b/web/src/pages/agents/agent-card.tsx @@ -1,8 +1,11 @@ import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; import { SharedBadge } from '@/components/shared-badge'; +import { Button } from '@/components/ui/button'; +import { AgentCategory } from '@/constants/agent'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { IFlow } from '@/interfaces/database/agent'; +import { Route } from 'lucide-react'; import { AgentDropdown } from './agent-dropdown'; import { useRenameAgent } from './use-rename-agent'; @@ -11,7 +14,7 @@ export type DatasetCardProps = { } & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>; export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { - const { navigateToAgent } = useNavigatePage(); + const { navigateToAgent, navigateToDataflow } = useNavigatePage(); return ( <HomeCard @@ -22,7 +25,18 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { </AgentDropdown> } sharedBadge={<SharedBadge>{data.nickname}</SharedBadge>} - onClick={navigateToAgent(data?.id)} + onClick={ + data.canvas_category === AgentCategory.DataflowCanvas + ? navigateToDataflow(data.id) + : navigateToAgent(data?.id) + } + icon={ + data.canvas_category === AgentCategory.DataflowCanvas && ( + <Button variant={'ghost'} size={'sm'}> + <Route /> + </Button> + ) + } /> ); } diff --git a/web/src/pages/agents/create-agent-dialog.tsx b/web/src/pages/agents/create-agent-dialog.tsx index c4b7fd467..5c8f99320 100644 --- a/web/src/pages/agents/create-agent-dialog.tsx +++ b/web/src/pages/agents/create-agent-dialog.tsx @@ -22,7 +22,7 @@ export function CreateAgentDialog({ return ( <Dialog open onOpenChange={hideModal}> - <DialogContent className="sm:max-w-[425px]"> + <DialogContent> <DialogHeader> <DialogTitle>{t('flow.createGraph')}</DialogTitle> </DialogHeader> diff --git a/web/src/pages/agents/create-agent-form.tsx b/web/src/pages/agents/create-agent-form.tsx index eed33fe2e..f89031d17 100644 --- a/web/src/pages/agents/create-agent-form.tsx +++ b/web/src/pages/agents/create-agent-form.tsx @@ -25,6 +25,7 @@ type FlowTypeCardProps = { onChange?: (value: FlowType) => void; }; function FlowTypeCards({ value, onChange }: FlowTypeCardProps) { + const { t } = useTranslation(); const handleChange = useCallback( (value: FlowType) => () => { onChange?.(value); @@ -59,7 +60,11 @@ function FlowTypeCards({ value, onChange }: FlowTypeCardProps) { ) : ( <Route className="size-6" /> )} - <p>{val}</p> + <p> + {t( + `flow.${val === FlowType.Agent ? 'createAgent' : 'createPipeline'}`, + )} + </p> </div> {isActive && <Check />} </CardContent> @@ -106,7 +111,11 @@ export function CreateAgentForm({ id={TagRenameId} > {shouldChooseAgent && ( - <RAGFlowFormItem required name="type" label={t('common.type')}> + <RAGFlowFormItem + required + name="type" + label={t('flow.chooseAgentType')} + > <FlowTypeCards></FlowTypeCards> </RAGFlowFormItem> )} diff --git a/web/src/pages/agents/hooks/use-create-agent.ts b/web/src/pages/agents/hooks/use-create-agent.ts index 3693ce4ec..da1915290 100644 --- a/web/src/pages/agents/hooks/use-create-agent.ts +++ b/web/src/pages/agents/hooks/use-create-agent.ts @@ -1,10 +1,81 @@ +import { AgentCategory } from '@/constants/agent'; import { useSetModalState } from '@/hooks/common-hooks'; import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request'; import { DSL } from '@/interfaces/database/agent'; +import { + BeginId, + Operator, + initialParserValues, +} from '@/pages/data-flow/constant'; import { useCallback } from 'react'; import { FlowType } from '../constant'; import { FormSchemaType } from '../create-agent-form'; +export const DataflowEmptyDsl = { + graph: { + nodes: [ + { + id: BeginId, + type: 'beginNode', + position: { + x: 50, + y: 200, + }, + data: { + label: Operator.Begin, + name: Operator.Begin, + }, + sourcePosition: 'left', + targetPosition: 'right', + }, + { + data: { + form: initialParserValues, + label: 'Parser', + name: 'Parser_0', + }, + dragging: false, + id: 'Parser:HipSignsRhyme', + measured: { + height: 57, + width: 200, + }, + position: { + x: 316.99524094206413, + y: 195.39629819663406, + }, + selected: true, + sourcePosition: 'right', + targetPosition: 'left', + type: 'parserNode', + }, + ], + edges: [ + { + id: 'xy-edge__Filestart-Parser:HipSignsRhymeend', + source: BeginId, + sourceHandle: 'start', + target: 'Parser:HipSignsRhyme', + targetHandle: 'end', + }, + ], + }, + components: { + [Operator.Begin]: { + obj: { + component_name: Operator.Begin, + params: {}, + }, + downstream: [], // other edge target is downstream, edge source is current node id + upstream: [], // edge source is upstream, edge target is current node id + }, + }, + retrieval: [], // reference + history: [], + path: [], + globals: {}, +}; + export function useCreateAgentOrPipeline() { const { loading, setAgent } = useSetAgent(); const { @@ -13,27 +84,26 @@ export function useCreateAgentOrPipeline() { showModal: showCreatingModal, } = useSetModalState(); - const createAgent = useCallback( - async (name: string) => { - return setAgent({ title: name, dsl: EmptyDsl as DSL }); - }, - [setAgent], - ); - const handleCreateAgentOrPipeline = useCallback( async (data: FormSchemaType) => { - if (data.type === FlowType.Agent) { - const ret = await createAgent(data.name); - if (ret.code === 0) { - hideCreatingModal(); - } + const isAgent = data.type === FlowType.Agent; + const ret = await setAgent({ + title: data.name, + dsl: isAgent ? (EmptyDsl as DSL) : (DataflowEmptyDsl as DSL), + canvas_category: isAgent + ? AgentCategory.AgentCanvas + : AgentCategory.DataflowCanvas, + }); + + if (ret.code === 0) { + hideCreatingModal(); } }, - [createAgent, hideCreatingModal], + [hideCreatingModal, setAgent], ); return { - loading, + loading: loading, creatingVisible, hideCreatingModal, showCreatingModal, diff --git a/web/src/pages/agents/hooks/use-selelct-filters.ts b/web/src/pages/agents/hooks/use-selelct-filters.ts new file mode 100644 index 000000000..e1ea755ee --- /dev/null +++ b/web/src/pages/agents/hooks/use-selelct-filters.ts @@ -0,0 +1,23 @@ +import { FilterCollection } from '@/components/list-filter-bar/interface'; +import { useFetchAgentList } from '@/hooks/use-agent-request'; +import { buildOwnersFilter, groupListByType } from '@/utils/list-filter-util'; +import { useMemo } from 'react'; + +export function useSelectFilters() { + const { data } = useFetchAgentList({}); + + const canvasCategory = useMemo(() => { + return groupListByType(data.canvas, 'canvas_category', 'canvas_category'); + }, [data.canvas]); + + const filters: FilterCollection[] = [ + buildOwnersFilter(data.canvas), + { + field: 'canvasCategory', + list: canvasCategory, + label: 'Canvas category', + }, + ]; + + return filters; +} diff --git a/web/src/pages/agents/index.tsx b/web/src/pages/agents/index.tsx index 5889753dd..ab2db3e3b 100644 --- a/web/src/pages/agents/index.tsx +++ b/web/src/pages/agents/index.tsx @@ -17,13 +17,21 @@ import { useCallback } from 'react'; import { AgentCard } from './agent-card'; import { CreateAgentDialog } from './create-agent-dialog'; import { useCreateAgentOrPipeline } from './hooks/use-create-agent'; +import { useSelectFilters } from './hooks/use-selelct-filters'; import { UploadAgentDialog } from './upload-agent-dialog'; import { useHandleImportJsonFile } from './use-import-json'; import { useRenameAgent } from './use-rename-agent'; export default function Agents() { - const { data, pagination, setPagination, searchString, handleInputChange } = - useFetchAgentListByPage(); + const { + data, + pagination, + setPagination, + searchString, + handleInputChange, + filterValue, + handleFilterSubmit, + } = useFetchAgentListByPage(); const { navigateToAgentTemplates } = useNavigatePage(); const { @@ -50,6 +58,8 @@ export default function Agents() { hideFileUploadModal, } = useHandleImportJsonFile(); + const filters = useSelectFilters(); + const handlePageChange = useCallback( (page: number, pageSize?: number) => { setPagination({ page, pageSize }); @@ -65,6 +75,9 @@ export default function Agents() { searchString={searchString} onSearchChange={handleInputChange} icon="agent" + filters={filters} + onChange={handleFilterSubmit} + value={filterValue} > <DropdownMenu> <DropdownMenuTrigger> @@ -79,21 +92,21 @@ export default function Agents() { onClick={showCreatingModal} > <Clipboard /> - Create from Blank + {t('flow.createFromBlank')} </DropdownMenuItem> <DropdownMenuItem justifyBetween={false} onClick={navigateToAgentTemplates} > <ClipboardPlus /> - Create from Template + {t('flow.createFromTemplate')} </DropdownMenuItem> <DropdownMenuItem justifyBetween={false} onClick={handleImportJson} > <FileInput /> - Import json file + {t('flow.importJsonFile')} </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> diff --git a/web/src/pages/agents/name-form-field.tsx b/web/src/pages/agents/name-form-field.tsx index f18e46924..6a17ae97c 100644 --- a/web/src/pages/agents/name-form-field.tsx +++ b/web/src/pages/agents/name-form-field.tsx @@ -16,12 +16,7 @@ export const NameFormSchema = { export function NameFormField() { const { t } = useTranslation(); return ( - <RAGFlowFormItem - name="name" - required - label={t('common.name')} - tooltip={t('flow.sqlStatementTip')} - > + <RAGFlowFormItem name="name" required label={t('common.name')}> <Input placeholder={t('common.namePlaceholder')} autoComplete="off" /> </RAGFlowFormItem> ); diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx index f5b1b751a..d90147819 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx @@ -72,7 +72,7 @@ const Chunk = () => { chunkUpdatingVisible, documentId, } = useUpdateChunk(); - const { navigateToDataset, getQueryString, navigateToDatasetList } = + const { navigateToDataFile, getQueryString, navigateToDatasetList } = useNavigatePage(); const fileUrl = useGetDocumentUrl(); useEffect(() => { @@ -188,7 +188,7 @@ const Chunk = () => { <BreadcrumbSeparator /> <BreadcrumbItem> <BreadcrumbLink - onClick={navigateToDataset( + onClick={navigateToDataFile( getQueryString(QueryStringMap.id) as string, )} > diff --git a/web/src/pages/data-flow/canvas/edge/index.tsx b/web/src/pages/data-flow/canvas/edge/index.tsx index 3e1b57e85..40c8bb14d 100644 --- a/web/src/pages/data-flow/canvas/edge/index.tsx +++ b/web/src/pages/data-flow/canvas/edge/index.tsx @@ -11,7 +11,7 @@ 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'; +import { Operator } from '../../constant'; function InnerButtonEdge({ id, @@ -27,7 +27,6 @@ function InnerButtonEdge({ markerEnd, selected, data, - sourceHandleId, }: EdgeProps<Edge<{ isHovered: boolean }>>) { const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById); const [edgePath, labelX, labelY] = getBezierPath({ @@ -49,47 +48,32 @@ function InnerButtonEdge({ // highlight the nodes that the workflow passes through const { data: flowDetail } = useFetchAgent(); - const graphPath = useMemo(() => { - // TODO: this will be called multiple times + const showHighlight = useMemo(() => { 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); + const idx = path.findIndex((x) => x === target); 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' }; + let index = idx - 1; + while (index >= 0) { + if (path[index] === source) { + return { strokeWidth: 1, stroke: 'var(--accent-primary)' }; + } + index--; } + return {}; } return {}; - }, [source, target, graphPath]); + }, [flowDetail?.dsl?.path, source, target]); 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 data?.isHovered && source !== Operator.Begin; + }, [data?.isHovered, source]); return ( <> <BaseEdge path={edgePath} markerEnd={markerEnd} - style={{ ...style, ...selectedStyle, ...highlightStyle }} + style={{ ...style, ...selectedStyle, ...showHighlight }} className="text-text-secondary" /> diff --git a/web/src/pages/data-flow/canvas/index.tsx b/web/src/pages/data-flow/canvas/index.tsx index 73ce3cb98..edc998a69 100644 --- a/web/src/pages/data-flow/canvas/index.tsx +++ b/web/src/pages/data-flow/canvas/index.tsx @@ -12,6 +12,7 @@ import { ControlButton, Controls, NodeTypes, + OnConnectEnd, Position, ReactFlow, ReactFlowInstance, @@ -30,58 +31,34 @@ import { useBeforeDelete } from '../hooks/use-before-delete'; import { useMoveNote } from '../hooks/use-move-note'; import { useDropdownManager } from './context'; +import { useRunDataflow } from '../hooks/use-run-dataflow'; import { useHideFormSheetOnNodeDeletion, useShowDrawer, } from '../hooks/use-show-drawer'; import RunSheet from '../run-sheet'; +import useGraphStore from '../store'; 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 ChunkerNode from './node/chunker-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 { NextStepDropdown } from './node/dropdown/next-step-dropdown'; +import { ExtractorNode } from './node/extractor-node'; +import { HierarchicalMergerNode } from './node/hierarchical-merger-node'; import NoteNode from './node/note-node'; import ParserNode from './node/parser-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 { SplitterNode } from './node/splitter-node'; import TokenizerNode from './node/tokenizer-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, parserNode: ParserNode, - chunkerNode: ChunkerNode, tokenizerNode: TokenizerNode, + splitterNode: SplitterNode, + hierarchicalMergerNode: HierarchicalMergerNode, + contextNode: ExtractorNode, }; const edgeTypes = { @@ -91,9 +68,10 @@ const edgeTypes = { interface IProps { drawerVisible: boolean; hideDrawer(): void; + showLogSheet(): void; } -function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { +function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) { const { t } = useTranslation(); const { nodes, @@ -121,7 +99,6 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { chatVisible, runVisible, hideRunOrChatDrawer, - showChatModal, showFormDrawer, } = useShowDrawer({ drawerVisible, @@ -141,6 +118,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { useHideFormSheetOnNodeDeletion({ hideFormDrawer }); const { visible, hideModal, showModal } = useSetModalState(); + const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 }); const isConnectedRef = useRef(false); @@ -153,6 +131,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { const { setActiveDropdown, clearActiveDropdown } = useDropdownManager(); + const { hasChildNode } = useGraphStore((state) => state); + const onPaneClick = useCallback(() => { hideFormDrawer(); if (visible && !preventCloseRef.current) { @@ -174,12 +154,17 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { clearActiveDropdown, ]); + const { run, loading: running } = useRunDataflow( + showLogSheet!, + hideRunOrChatDrawer, + ); + const onConnect = (connection: Connection) => { originalOnConnect(connection); isConnectedRef.current = true; }; - const OnConnectStart = (event: any, params: any) => { + const onConnectStart = (event: any, params: any) => { isConnectedRef.current = false; if (params && params.nodeId && params.handleId) { @@ -192,7 +177,12 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { } }; - const OnConnectEnd = (event: MouseEvent | TouchEvent) => { + const onConnectEnd: OnConnectEnd = (event, connectionState) => { + const nodeId = connectionState.fromNode?.id; + // Events triggered by Handle are directly interrupted + if (connectionState.toNode !== null || (nodeId && hasChildNode(nodeId))) { + return; + } if ('clientX' in event && 'clientY' in event) { const { clientX, clientY } = event; setDropdownPosition({ x: clientX, y: clientY }); @@ -240,8 +230,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { onConnect={onConnect} nodeTypes={nodeTypes} edgeTypes={edgeTypes} - onConnectStart={OnConnectStart} - onConnectEnd={OnConnectEnd} + onConnectStart={onConnectStart} + onConnectEnd={onConnectEnd} onNodeClick={onNodeClick} onPaneClick={onPaneClick} onInit={setReactFlowInstance} @@ -288,7 +278,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { isFromConnectionDrag: true, }} > - <InnerNextStepDropdown + <NextStepDropdown hideModal={() => { hideModal(); clearActiveDropdown(); @@ -296,7 +286,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { position={dropdownPosition} > <span></span> - </InnerNextStepDropdown> + </NextStepDropdown> </HandleContext.Provider> )} </AgentInstanceContext.Provider> @@ -322,7 +312,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { {runVisible && ( <RunSheet hideModal={hideRunOrChatDrawer} - showModal={showChatModal} + run={run} + loading={running} ></RunSheet> )} </div> diff --git a/web/src/pages/data-flow/canvas/node/agent-node.tsx b/web/src/pages/data-flow/canvas/node/agent-node.tsx deleted file mode 100644 index 42b489a41..000000000 --- a/web/src/pages/data-flow/canvas/node/agent-node.tsx +++ /dev/null @@ -1,116 +0,0 @@ -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<IAgentNode>) { - 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 ( - <ToolBar selected={selected} id={id} label={data.label}> - <NodeWrapper selected={selected}> - {isHeadAgent && ( - <> - <CommonHandle - type="target" - position={Position.Left} - isConnectable={isConnectable} - style={LeftHandleStyle} - nodeId={id} - id={NodeHandleId.End} - ></CommonHandle> - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - nodeId={id} - id={NodeHandleId.Start} - isConnectableEnd={false} - ></CommonHandle> - </> - )} - - <Handle - type="target" - position={Position.Top} - isConnectable={false} - id={NodeHandleId.AgentTop} - ></Handle> - <Handle - type="source" - position={Position.Bottom} - isConnectable={false} - id={NodeHandleId.AgentBottom} - style={{ left: 180 }} - ></Handle> - <Handle - type="source" - position={Position.Bottom} - isConnectable={false} - id={NodeHandleId.Tool} - style={{ left: 20 }} - ></Handle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - <section className="flex flex-col gap-2"> - <div className={'bg-bg-card rounded-sm p-1'}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - {(isGotoMethod || - exceptionMethod === AgentExceptionMethod.Comment) && ( - <div className="bg-bg-card rounded-sm p-1 flex justify-between gap-2"> - <span className="text-text-secondary">{t('flow.onFailure')}</span> - <span className="truncate flex-1 text-right"> - {t(`flow.${exceptionMethod}`)} - </span> - </div> - )} - </section> - {isGotoMethod && ( - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className="!bg-state-error" - style={{ ...RightHandleStyle, top: 94 }} - nodeId={id} - id={NodeHandleId.AgentException} - isConnectableEnd={false} - ></CommonHandle> - )} - </NodeWrapper> - </ToolBar> - ); -} - -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 index be80d56ae..2d3db3cba 100644 --- a/web/src/pages/data-flow/canvas/node/begin-node.tsx +++ b/web/src/pages/data-flow/canvas/node/begin-node.tsx @@ -36,7 +36,7 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) { <section className="flex items-center gap-2"> <OperatorIcon name={data.label as Operator}></OperatorIcon> <div className="truncate text-center font-semibold text-sm"> - {t(`flow.begin`)} + {t(`dataflow.begin`)} </div> </section> <section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}> diff --git a/web/src/pages/data-flow/canvas/node/categorize-node.tsx b/web/src/pages/data-flow/canvas/node/categorize-node.tsx deleted file mode 100644 index a54136b5a..000000000 --- a/web/src/pages/data-flow/canvas/node/categorize-node.tsx +++ /dev/null @@ -1,62 +0,0 @@ -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<ICategorizeNode>) { - const { positions } = useBuildCategorizeHandlePositions({ data, id }); - return ( - <ToolBar selected={selected} id={id} label={data.label}> - <NodeWrapper selected={selected}> - <CommonHandle - type="target" - position={Position.Left} - isConnectable - id={NodeHandleId.End} - nodeId={id} - ></CommonHandle> - - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - - <section className="flex flex-col gap-2"> - <div className={'bg-bg-card rounded-sm px-1'}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - {positions.map((position) => { - return ( - <div key={position.uuid}> - <div className={'bg-bg-card rounded-sm p-1 truncate'}> - {position.name} - </div> - <CommonHandle - // key={position.text} - id={position.uuid} - type="source" - position={Position.Right} - isConnectable - style={{ ...RightHandleStyle, top: position.top }} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> - </div> - ); - })} - </section> - </NodeWrapper> - </ToolBar> - ); -} - -export const CategorizeNode = memo(InnerCategorizeNode); diff --git a/web/src/pages/data-flow/canvas/node/chunker-node.tsx b/web/src/pages/data-flow/canvas/node/chunker-node.tsx deleted file mode 100644 index a6866b442..000000000 --- a/web/src/pages/data-flow/canvas/node/chunker-node.tsx +++ /dev/null @@ -1,49 +0,0 @@ -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 ChunkerNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IRagNode>) { - return ( - <ToolBar - selected={selected} - id={id} - label={data.label} - showRun={needsSingleStepDebugging(data.label)} - > - <NodeWrapper selected={selected}> - <CommonHandle - id={NodeHandleId.End} - type="target" - position={Position.Left} - isConnectable={isConnectable} - style={LeftHandleStyle} - nodeId={id} - ></CommonHandle> - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - id={NodeHandleId.Start} - style={RightHandleStyle} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - </NodeWrapper> - </ToolBar> - ); -} - -export default memo(ChunkerNode); 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 index 641eec7ae..2e9c95f26 100644 --- 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 @@ -1,9 +1,3 @@ -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from '@/components/ui/accordion'; import { DropdownMenu, DropdownMenuContent, @@ -18,6 +12,7 @@ import { } from '@/components/ui/tooltip'; import { IModalProps } from '@/interfaces/common'; import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks'; +import useGraphStore from '@/pages/data-flow/store'; import { Position } from '@xyflow/react'; import { t } from 'i18next'; import { @@ -26,9 +21,10 @@ import { memo, useContext, useEffect, + useMemo, useRef, } from 'react'; -import { Operator } from '../../../constant'; +import { Operator, SingleOperators } from '../../../constant'; import { AgentInstanceContext, HandleContext } from '../../../context'; import OperatorIcon from '../../../operator-icon'; @@ -116,6 +112,20 @@ function OperatorItemList({ return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>; } +// Limit the number of operators of a certain type on the canvas to only one +function useRestrictSingleOperatorOnCanvas() { + const list: Operator[] = []; + const { findNodeByName } = useGraphStore((state) => state); + + SingleOperators.forEach((operator) => { + if (!findNodeByName(operator)) { + list.push(operator); + } + }); + + return list; +} + function AccordionOperators({ isCustomDropdown = false, mousePosition, @@ -123,83 +133,19 @@ function AccordionOperators({ isCustomDropdown?: boolean; mousePosition?: { x: number; y: number }; }) { + const singleOperators = useRestrictSingleOperatorOnCanvas(); + const operators = useMemo(() => { + const list = [...singleOperators]; + list.push(Operator.Extractor); + return list; + }, [singleOperators]); + return ( - <Accordion - type="multiple" - className="px-2 text-text-title max-h-[45vh] overflow-auto" - defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']} - > - <AccordionItem value="item-1"> - <AccordionTrigger className="text-xl"> - {t('flow.foundation')} - </AccordionTrigger> - <AccordionContent className="flex flex-col gap-4 text-balance"> - <OperatorItemList - operators={[ - Operator.Agent, - Operator.Retrieval, - Operator.Parser, - Operator.Chunker, - Operator.Tokenizer, - ]} - isCustomDropdown={isCustomDropdown} - mousePosition={mousePosition} - ></OperatorItemList> - </AccordionContent> - </AccordionItem> - <AccordionItem value="item-2"> - <AccordionTrigger className="text-xl"> - {t('flow.dialog')} - </AccordionTrigger> - <AccordionContent className="flex flex-col gap-4 text-balance"> - <OperatorItemList - operators={[Operator.Message, Operator.UserFillUp]} - isCustomDropdown={isCustomDropdown} - mousePosition={mousePosition} - ></OperatorItemList> - </AccordionContent> - </AccordionItem> - <AccordionItem value="item-3"> - <AccordionTrigger className="text-xl"> - {t('flow.flow')} - </AccordionTrigger> - <AccordionContent className="flex flex-col gap-4 text-balance"> - <OperatorItemList - operators={[ - Operator.Switch, - Operator.Iteration, - Operator.Categorize, - ]} - isCustomDropdown={isCustomDropdown} - mousePosition={mousePosition} - ></OperatorItemList> - </AccordionContent> - </AccordionItem> - <AccordionItem value="item-4"> - <AccordionTrigger className="text-xl"> - {t('flow.dataManipulation')} - </AccordionTrigger> - <AccordionContent className="flex flex-col gap-4 text-balance"> - <OperatorItemList - operators={[Operator.Code, Operator.StringTransform]} - isCustomDropdown={isCustomDropdown} - mousePosition={mousePosition} - ></OperatorItemList> - </AccordionContent> - </AccordionItem> - <AccordionItem value="item-5"> - <AccordionTrigger className="text-xl"> - {t('flow.tools')} - </AccordionTrigger> - <AccordionContent className="flex flex-col gap-4 text-balance"> - <OperatorItemList - operators={[Operator.ExeSQL, Operator.Email, Operator.Invoke]} - isCustomDropdown={isCustomDropdown} - mousePosition={mousePosition} - ></OperatorItemList> - </AccordionContent> - </AccordionItem> - </Accordion> + <OperatorItemList + operators={operators} + isCustomDropdown={isCustomDropdown} + mousePosition={mousePosition} + ></OperatorItemList> ); } diff --git a/web/src/pages/data-flow/canvas/node/email-node.tsx b/web/src/pages/data-flow/canvas/node/email-node.tsx deleted file mode 100644 index 9482194f3..000000000 --- a/web/src/pages/data-flow/canvas/node/email-node.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { IEmailNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { memo, useState } from 'react'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function InnerEmailNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IEmailNode>) { - const [showDetails, setShowDetails] = useState(false); - - return ( - <section - className={classNames(styles.ragNode, { - [styles.selectedNode]: selected, - })} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - - <Flex vertical gap={8} className={styles.emailNodeContainer}> - <div - className={styles.emailConfig} - onClick={() => setShowDetails(!showDetails)} - > - <div className={styles.configItem}> - <span className={styles.configLabel}>SMTP:</span> - <span className={styles.configValue}>{data.form?.smtp_server}</span> - </div> - <div className={styles.configItem}> - <span className={styles.configLabel}>Port:</span> - <span className={styles.configValue}>{data.form?.smtp_port}</span> - </div> - <div className={styles.configItem}> - <span className={styles.configLabel}>From:</span> - <span className={styles.configValue}>{data.form?.email}</span> - </div> - <div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div> - </div> - - {showDetails && ( - <div className={styles.jsonExample}> - <div className={styles.jsonTitle}>Expected Input JSON:</div> - <pre className={styles.jsonContent}> - {`{ - "to_email": "...", - "cc_email": "...", - "subject": "...", - "content": "..." -}`} - </pre> - </div> - )} - </Flex> - </section> - ); -} - -export const EmailNode = memo(InnerEmailNode); diff --git a/web/src/pages/data-flow/canvas/node/extractor-node.tsx b/web/src/pages/data-flow/canvas/node/extractor-node.tsx new file mode 100644 index 000000000..b8746a163 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/extractor-node.tsx @@ -0,0 +1 @@ +export { RagNode as ExtractorNode } from './index'; diff --git a/web/src/pages/data-flow/canvas/node/generate-node.tsx b/web/src/pages/data-flow/canvas/node/generate-node.tsx deleted file mode 100644 index 8ffbbd79c..000000000 --- a/web/src/pages/data-flow/canvas/node/generate-node.tsx +++ /dev/null @@ -1,60 +0,0 @@ -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<IGenerateNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - </section> - ); -} - -export const GenerateNode = memo(InnerGenerateNode); diff --git a/web/src/pages/data-flow/canvas/node/handle.tsx b/web/src/pages/data-flow/canvas/node/handle.tsx index 71b473cc7..da61e3b4c 100644 --- a/web/src/pages/data-flow/canvas/node/handle.tsx +++ b/web/src/pages/data-flow/canvas/node/handle.tsx @@ -4,8 +4,9 @@ import { Handle, HandleProps } from '@xyflow/react'; import { Plus } from 'lucide-react'; import { useMemo } from 'react'; import { HandleContext } from '../../context'; +import useGraphStore from '../../store'; import { useDropdownManager } from '../context'; -import { InnerNextStepDropdown } from './dropdown/next-step-dropdown'; +import { NextStepDropdown } from './dropdown/next-step-dropdown'; export function CommonHandle({ className, @@ -17,6 +18,8 @@ export function CommonHandle({ const { canShowDropdown, setActiveDropdown, clearActiveDropdown } = useDropdownManager(); + const { hasChildNode } = useGraphStore((state) => state); + const value = useMemo( () => ({ nodeId, @@ -39,6 +42,10 @@ export function CommonHandle({ onClick={(e) => { e.stopPropagation(); + if (hasChildNode(nodeId)) { + return; + } + if (!canShowDropdown()) { return; } @@ -49,14 +56,14 @@ export function CommonHandle({ > <Plus className="size-3 pointer-events-none text-text-title-invert" /> {visible && ( - <InnerNextStepDropdown + <NextStepDropdown hideModal={() => { hideModal(); clearActiveDropdown(); }} > <span></span> - </InnerNextStepDropdown> + </NextStepDropdown> )} </Handle> </HandleContext.Provider> diff --git a/web/src/pages/data-flow/canvas/node/hierarchical-merger-node.tsx b/web/src/pages/data-flow/canvas/node/hierarchical-merger-node.tsx new file mode 100644 index 000000000..ba47dccfb --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/hierarchical-merger-node.tsx @@ -0,0 +1 @@ +export { RagNode as HierarchicalMergerNode } from './index'; diff --git a/web/src/pages/data-flow/canvas/node/index.tsx b/web/src/pages/data-flow/canvas/node/index.tsx index a1d48955b..7a26d4ecc 100644 --- a/web/src/pages/data-flow/canvas/node/index.tsx +++ b/web/src/pages/data-flow/canvas/node/index.tsx @@ -1,8 +1,8 @@ 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 { memo, useMemo } from 'react'; +import { NodeHandleId, SingleOperators } from '../../constant'; +import useGraphStore from '../../store'; import { CommonHandle } from './handle'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import NodeHeader from './node-header'; @@ -15,13 +15,17 @@ function InnerRagNode({ isConnectable = true, selected, }: NodeProps<IRagNode>) { + const getOperatorTypeFromId = useGraphStore( + (state) => state.getOperatorTypeFromId, + ); + + const showCopy = useMemo(() => { + const operatorName = getOperatorTypeFromId(id); + return SingleOperators.every((x) => x !== operatorName); + }, [getOperatorTypeFromId, id]); + return ( - <ToolBar - selected={selected} - id={id} - label={data.label} - showRun={needsSingleStepDebugging(data.label)} - > + <ToolBar selected={selected} id={id} label={data.label} showCopy={showCopy}> <NodeWrapper selected={selected}> <CommonHandle id={NodeHandleId.End} diff --git a/web/src/pages/data-flow/canvas/node/invoke-node.tsx b/web/src/pages/data-flow/canvas/node/invoke-node.tsx deleted file mode 100644 index cf1e28d02..000000000 --- a/web/src/pages/data-flow/canvas/node/invoke-node.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { IInvokeNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -function InnerInvokeNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IInvokeNode>) { - const { t } = useTranslation(); - const { theme } = useTheme(); - const url = get(data, 'form.url'); - return ( - <section - className={classNames( - styles.ragNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - id="b" - style={RightHandleStyle} - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - <Flex vertical> - <div>{t('flow.url')}</div> - <div className={styles.nodeText}>{url}</div> - </Flex> - </section> - ); -} - -export const InvokeNode = memo(InnerInvokeNode); diff --git a/web/src/pages/data-flow/canvas/node/iteration-node.tsx b/web/src/pages/data-flow/canvas/node/iteration-node.tsx deleted file mode 100644 index 3bdbae590..000000000 --- a/web/src/pages/data-flow/canvas/node/iteration-node.tsx +++ /dev/null @@ -1,93 +0,0 @@ -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<IIterationNode>) { - return ( - <ToolBar selected={selected} id={id} label={data.label} showRun={false}> - <section - className={cn('h-full bg-transparent rounded-b-md ', { - [styles.selectedHeader]: selected, - })} - > - <NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}> - <ResizeIcon /> - </NodeResizeControl> - <CommonHandle - id={NodeHandleId.End} - type="target" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - nodeId={id} - ></CommonHandle> - <CommonHandle - id={NodeHandleId.Start} - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - nodeId={id} - ></CommonHandle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - wrapperClassName={cn( - 'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]', - { - [styles.selectedHeader]: selected, - }, - )} - ></NodeHeader> - </section> - </ToolBar> - ); -} - -function InnerIterationStartNode({ - isConnectable = true, - id, - selected, -}: NodeProps<IIterationStartNode>) { - return ( - <NodeWrapper className="w-20" selected={selected}> - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - isConnectableEnd={false} - id={NodeHandleId.Start} - nodeId={id} - ></CommonHandle> - <div> - <OperatorIcon name={Operator.Begin}></OperatorIcon> - </div> - </NodeWrapper> - ); -} - -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 deleted file mode 100644 index 012dcf26c..000000000 --- a/web/src/pages/data-flow/canvas/node/keyword-node.tsx +++ /dev/null @@ -1,60 +0,0 @@ -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<IKeywordNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - </section> - ); -} - -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 deleted file mode 100644 index 481c26c25..000000000 --- a/web/src/pages/data-flow/canvas/node/logic-node.tsx +++ /dev/null @@ -1,41 +0,0 @@ -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<ILogicNode>) { - return ( - <ToolBar selected={selected} id={id} label={data.label}> - <NodeWrapper selected={selected}> - <CommonHandle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - style={LeftHandleStyle} - nodeId={id} - ></CommonHandle> - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - style={RightHandleStyle} - id="b" - nodeId={id} - ></CommonHandle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - </NodeWrapper> - </ToolBar> - ); -} - -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 deleted file mode 100644 index 057845a63..000000000 --- a/web/src/pages/data-flow/canvas/node/message-node.tsx +++ /dev/null @@ -1,65 +0,0 @@ -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<IMessageNode>) { - const messages: string[] = get(data, 'form.messages', []); - return ( - <ToolBar selected={selected} id={id} label={data.label}> - <NodeWrapper selected={selected}> - <CommonHandle - type="target" - position={Position.Left} - isConnectable={isConnectable} - style={LeftHandleStyle} - nodeId={id} - id={NodeHandleId.End} - ></CommonHandle> - {/* <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - style={RightHandleStyle} - id={NodeHandleId.Start} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> */} - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={classNames({ - [styles.nodeHeader]: messages.length > 0, - })} - ></NodeHeader> - - <Flex vertical gap={8} className={styles.messageNodeContainer}> - {messages.map((message, idx) => { - return ( - <div className={styles.nodeText} key={idx}> - {message} - </div> - ); - })} - </Flex> - </NodeWrapper> - </ToolBar> - ); -} - -export const MessageNode = memo(InnerMessageNode); diff --git a/web/src/pages/data-flow/canvas/node/parser-node.tsx b/web/src/pages/data-flow/canvas/node/parser-node.tsx index 4dceabce9..512632db6 100644 --- a/web/src/pages/data-flow/canvas/node/parser-node.tsx +++ b/web/src/pages/data-flow/canvas/node/parser-node.tsx @@ -2,12 +2,10 @@ 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 ParserNode({ id, @@ -16,33 +14,26 @@ function ParserNode({ selected, }: NodeProps<IRagNode>) { return ( - <ToolBar - selected={selected} - id={id} - label={data.label} - showRun={needsSingleStepDebugging(data.label)} - > - <NodeWrapper selected={selected}> - <CommonHandle - id={NodeHandleId.End} - type="target" - position={Position.Left} - isConnectable={isConnectable} - style={LeftHandleStyle} - nodeId={id} - ></CommonHandle> - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - id={NodeHandleId.Start} - style={RightHandleStyle} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - </NodeWrapper> - </ToolBar> + <NodeWrapper selected={selected}> + <CommonHandle + id={NodeHandleId.End} + type="target" + position={Position.Left} + isConnectable={isConnectable} + style={LeftHandleStyle} + nodeId={id} + ></CommonHandle> + <CommonHandle + type="source" + position={Position.Right} + isConnectable={isConnectable} + id={NodeHandleId.Start} + style={RightHandleStyle} + nodeId={id} + isConnectableEnd={false} + ></CommonHandle> + <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> + </NodeWrapper> ); } diff --git a/web/src/pages/data-flow/canvas/node/relevant-node.tsx b/web/src/pages/data-flow/canvas/node/relevant-node.tsx deleted file mode 100644 index 410a7accd..000000000 --- a/web/src/pages/data-flow/canvas/node/relevant-node.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { RightHandleStyle } from './handle-icon'; - -import { useTheme } from '@/components/theme-provider'; -import { IRelevantNode } from '@/interfaces/database/flow'; -import { get } from 'lodash'; -import { memo } from 'react'; -import { useReplaceIdWithName } from '../../hooks'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -function InnerRelevantNode({ id, data, selected }: NodeProps<IRelevantNode>) { - const yes = get(data, 'form.yes'); - const no = get(data, 'form.no'); - const replaceIdWithName = useReplaceIdWithName(); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - type="target" - position={Position.Left} - isConnectable - className={styles.handle} - id={'a'} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - id={'yes'} - style={{ ...RightHandleStyle, top: 57 + 20 }} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - id={'no'} - style={{ ...RightHandleStyle, top: 115 + 20 }} - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <Flex vertical gap={10}> - <Flex vertical> - <div className={styles.relevantLabel}>Yes</div> - <div className={styles.nodeText}>{replaceIdWithName(yes)}</div> - </Flex> - <Flex vertical> - <div className={styles.relevantLabel}>No</div> - <div className={styles.nodeText}>{replaceIdWithName(no)}</div> - </Flex> - </Flex> - </section> - ); -} - -export const RelevantNode = memo(InnerRelevantNode); diff --git a/web/src/pages/data-flow/canvas/node/retrieval-node.tsx b/web/src/pages/data-flow/canvas/node/retrieval-node.tsx deleted file mode 100644 index 5703ec1f4..000000000 --- a/web/src/pages/data-flow/canvas/node/retrieval-node.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { RAGFlowAvatar } from '@/components/ragflow-avatar'; -import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; -import { IRetrievalNode } from '@/interfaces/database/flow'; -import { NodeProps, Position } from '@xyflow/react'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { memo } from 'react'; -import { NodeHandleId } from '../../constant'; -import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query'; -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 InnerRetrievalNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IRetrievalNode>) { - const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); - const { list: knowledgeList } = useFetchKnowledgeList(true); - - const getLabel = useGetVariableLabelByValue(id); - - return ( - <ToolBar selected={selected} id={id} label={data.label}> - <NodeWrapper selected={selected}> - <CommonHandle - id={NodeHandleId.End} - type="target" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - nodeId={id} - ></CommonHandle> - <CommonHandle - id={NodeHandleId.Start} - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={classNames({ - [styles.nodeHeader]: knowledgeBaseIds.length > 0, - })} - ></NodeHeader> - <section className="flex flex-col gap-2"> - {knowledgeBaseIds.map((id) => { - const item = knowledgeList.find((y) => id === y.id); - const label = getLabel(id); - - return ( - <div className={styles.nodeText} key={id}> - <div className="flex items-center gap-1.5"> - <RAGFlowAvatar - className="size-6 rounded-lg" - avatar={id} - name={item?.name || (label as string) || 'CN'} - isPerson={true} - /> - - <div className={'truncate flex-1'}>{label || item?.name}</div> - </div> - </div> - ); - })} - </section> - </NodeWrapper> - </ToolBar> - ); -} - -export const RetrievalNode = memo(InnerRetrievalNode); diff --git a/web/src/pages/data-flow/canvas/node/rewrite-node.tsx b/web/src/pages/data-flow/canvas/node/rewrite-node.tsx deleted file mode 100644 index 134899c8b..000000000 --- a/web/src/pages/data-flow/canvas/node/rewrite-node.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import LLMLabel from '@/components/llm-select/llm-label'; -import { useTheme } from '@/components/theme-provider'; -import { IRewriteNode } 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'; - -function InnerRewriteNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IRewriteNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - </section> - ); -} - -export const RewriteNode = memo(InnerRewriteNode); diff --git a/web/src/pages/data-flow/canvas/node/splitter-node.tsx b/web/src/pages/data-flow/canvas/node/splitter-node.tsx new file mode 100644 index 000000000..c9948d3c6 --- /dev/null +++ b/web/src/pages/data-flow/canvas/node/splitter-node.tsx @@ -0,0 +1 @@ +export { RagNode as SplitterNode } from './index'; diff --git a/web/src/pages/data-flow/canvas/node/switch-node.tsx b/web/src/pages/data-flow/canvas/node/switch-node.tsx deleted file mode 100644 index b62e45adb..000000000 --- a/web/src/pages/data-flow/canvas/node/switch-node.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Card, CardContent } from '@/components/ui/card'; -import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; -import { NodeProps, Position } from '@xyflow/react'; -import { memo, useCallback } from 'react'; -import { NodeHandleId, SwitchOperatorOptions } from '../../constant'; -import { LogicalOperatorIcon } from '../../form/switch-form'; -import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query'; -import { CommonHandle } from './handle'; -import { RightHandleStyle } from './handle-icon'; -import NodeHeader from './node-header'; -import { NodeWrapper } from './node-wrapper'; -import { ToolBar } from './toolbar'; -import { useBuildSwitchHandlePositions } from './use-build-switch-handle-positions'; - -const getConditionKey = (idx: number, length: number) => { - if (idx === 0 && length !== 1) { - return 'If'; - } else if (idx === length - 1) { - return 'Else'; - } - - return 'ElseIf'; -}; - -const ConditionBlock = ({ - condition, - nodeId, -}: { condition: ISwitchCondition } & { nodeId: string }) => { - const items = condition?.items ?? []; - const getLabel = useGetVariableLabelByValue(nodeId); - - const renderOperatorIcon = useCallback((operator?: string) => { - const item = SwitchOperatorOptions.find((x) => x.value === operator); - if (item) { - return ( - <LogicalOperatorIcon - icon={item?.icon} - value={item?.value} - ></LogicalOperatorIcon> - ); - } - return <></>; - }, []); - - return ( - <Card> - <CardContent className="p-0 divide-y divide-background-card"> - {items.map((x, idx) => ( - <div key={idx}> - <section className="flex justify-between gap-2 items-center text-xs p-1"> - <div className="flex-1 truncate text-accent-primary"> - {getLabel(x?.cpn_id)} - </div> - <span>{renderOperatorIcon(x?.operator)}</span> - <div className="flex-1 truncate">{x?.value}</div> - </section> - </div> - ))} - </CardContent> - </Card> - ); -}; - -function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { - const { positions } = useBuildSwitchHandlePositions({ data, id }); - return ( - <ToolBar selected={selected} id={id} label={data.label} showRun={false}> - <NodeWrapper selected={selected}> - <CommonHandle - type="target" - position={Position.Left} - isConnectable - nodeId={id} - id={NodeHandleId.End} - ></CommonHandle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - <section className="gap-2.5 flex flex-col"> - {positions.map((position, idx) => { - return ( - <div key={idx}> - <section className="flex flex-col text-xs"> - <div className="text-right"> - <span>{getConditionKey(idx, positions.length)}</span> - <div className="text-text-secondary"> - {idx < positions.length - 1 && position.text} - </div> - </div> - <span className="text-accent-primary"> - {idx < positions.length - 1 && - position.condition?.logical_operator?.toUpperCase()} - </span> - {position.condition && ( - <ConditionBlock - condition={position.condition} - nodeId={id} - ></ConditionBlock> - )} - </section> - <CommonHandle - key={position.text} - id={position.text} - type="source" - position={Position.Right} - isConnectable - style={{ ...RightHandleStyle, top: position.top }} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> - </div> - ); - })} - </section> - </NodeWrapper> - </ToolBar> - ); -} - -export const SwitchNode = memo(InnerSwitchNode); diff --git a/web/src/pages/data-flow/canvas/node/template-node.tsx b/web/src/pages/data-flow/canvas/node/template-node.tsx deleted file mode 100644 index b204717ab..000000000 --- a/web/src/pages/data-flow/canvas/node/template-node.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; -import { IGenerateParameter } from '../../interface'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import NodeHeader from './node-header'; - -import { ITemplateNode } from '@/interfaces/database/flow'; -import { memo } from 'react'; -import styles from './index.less'; - -function InnerTemplateNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<ITemplateNode>) { - const parameters: IGenerateParameter[] = get(data, 'form.parameters', []); - const getLabel = useGetComponentLabelByValue(id); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <Flex gap={8} vertical className={styles.generateParameters}> - {parameters.map((x) => ( - <Flex - key={x.id} - align="center" - gap={6} - className={styles.conditionBlock} - > - <label htmlFor="">{x.key}</label> - <span className={styles.parameterValue}> - {getLabel(x.component_id)} - </span> - </Flex> - ))} - </Flex> - </section> - ); -} - -export const TemplateNode = memo(InnerTemplateNode); diff --git a/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx b/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx index 141399b14..710ddef1f 100644 --- a/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx +++ b/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx @@ -2,9 +2,8 @@ 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 { LeftHandleStyle } from './handle-icon'; import NodeHeader from './node-header'; import { NodeWrapper } from './node-wrapper'; import { ToolBar } from './toolbar'; @@ -20,7 +19,8 @@ function TokenizerNode({ selected={selected} id={id} label={data.label} - showRun={needsSingleStepDebugging(data.label)} + showRun={false} + showCopy={false} > <NodeWrapper selected={selected}> <CommonHandle @@ -31,15 +31,6 @@ function TokenizerNode({ style={LeftHandleStyle} nodeId={id} ></CommonHandle> - <CommonHandle - type="source" - position={Position.Right} - isConnectable={isConnectable} - id={NodeHandleId.Start} - style={RightHandleStyle} - nodeId={id} - isConnectableEnd={false} - ></CommonHandle> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> </NodeWrapper> </ToolBar> diff --git a/web/src/pages/data-flow/canvas/node/tool-node.tsx b/web/src/pages/data-flow/canvas/node/tool-node.tsx deleted file mode 100644 index 42c7bf3b5..000000000 --- a/web/src/pages/data-flow/canvas/node/tool-node.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { IAgentForm, IToolNode } from '@/interfaces/database/agent'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { get } from 'lodash'; -import { MouseEventHandler, memo, useCallback } from 'react'; -import { NodeHandleId, Operator } from '../../constant'; -import { ToolCard } from '../../form/agent-form/agent-tools'; -import { useFindMcpById } from '../../hooks/use-find-mcp-by-id'; -import OperatorIcon from '../../operator-icon'; -import useGraphStore from '../../store'; -import { NodeWrapper } from './node-wrapper'; - -function InnerToolNode({ - id, - isConnectable = true, - selected, -}: NodeProps<IToolNode>) { - const { edges, getNode } = useGraphStore((state) => state); - const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source; - const upstreamAgentNode = getNode(upstreamAgentNodeId); - const { findMcpById } = useFindMcpById(); - - const handleClick = useCallback( - (operator: string): MouseEventHandler<HTMLLIElement> => - (e) => { - if (operator === Operator.Code) { - e.preventDefault(); - e.stopPropagation(); - } - }, - [], - ); - - const tools: IAgentForm['tools'] = get( - upstreamAgentNode, - 'data.form.tools', - [], - ); - - const mcpList: IAgentForm['mcp'] = get( - upstreamAgentNode, - 'data.form.mcp', - [], - ); - - return ( - <NodeWrapper selected={selected}> - <Handle - id={NodeHandleId.End} - type="target" - position={Position.Top} - isConnectable={isConnectable} - ></Handle> - <ul className="space-y-2"> - {tools.map((x) => ( - <ToolCard - key={x.component_name} - onClick={handleClick(x.component_name)} - className="cursor-pointer" - data-tool={x.component_name} - > - <div className="flex gap-1 items-center pointer-events-none"> - <OperatorIcon name={x.component_name as Operator}></OperatorIcon> - {x.component_name} - </div> - </ToolCard> - ))} - - {mcpList.map((x) => ( - <ToolCard - key={x.mcp_id} - onClick={handleClick(x.mcp_id)} - className="cursor-pointer" - data-tool={x.mcp_id} - > - {findMcpById(x.mcp_id)?.name} - </ToolCard> - ))} - </ul> - </NodeWrapper> - ); -} - -export const ToolNode = memo(InnerToolNode); diff --git a/web/src/pages/data-flow/canvas/node/toolbar.tsx b/web/src/pages/data-flow/canvas/node/toolbar.tsx index a8b5cd13c..58b8261e4 100644 --- a/web/src/pages/data-flow/canvas/node/toolbar.tsx +++ b/web/src/pages/data-flow/canvas/node/toolbar.tsx @@ -11,7 +11,6 @@ import { PropsWithChildren, useCallback, } from 'react'; -import { Operator } from '../../constant'; import { useDuplicateNode } from '../../hooks'; import useGraphStore from '../../store'; @@ -28,6 +27,7 @@ type ToolBarProps = { label: string; id: string; showRun?: boolean; + showCopy?: boolean; } & PropsWithChildren; export function ToolBar({ @@ -35,23 +35,17 @@ export function ToolBar({ children, label, id, - showRun = true, + showRun = false, + showCopy = true, }: ToolBarProps) { const deleteNodeById = useGraphStore((store) => store.deleteNodeById); - const deleteIterationNodeById = useGraphStore( - (store) => store.deleteIterationNodeById, - ); const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback( (e) => { e.stopPropagation(); - if (label === Operator.Iteration) { - deleteIterationNodeById(id); - } else { - deleteNodeById(id); - } + deleteNodeById(id); }, - [deleteIterationNodeById, deleteNodeById, id, label], + [deleteNodeById, id], ); const duplicateNode = useDuplicateNode(); @@ -74,10 +68,13 @@ export function ToolBar({ <IconWrapper> <Play className="size-3.5" data-play /> </IconWrapper> - )}{' '} - <IconWrapper onClick={handleDuplicate}> - <Copy className="size-3.5" /> - </IconWrapper> + )} + {showCopy && ( + <IconWrapper onClick={handleDuplicate}> + <Copy className="size-3.5" /> + </IconWrapper> + )} + <IconWrapper onClick={deleteNode}> <Trash2 className="size-3.5" /> </IconWrapper> diff --git a/web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts b/web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts deleted file mode 100644 index 9973441e7..000000000 --- a/web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { useUpdateNodeInternals } from '@xyflow/react'; -import { get } from 'lodash'; -import { useEffect, useMemo } from 'react'; -import { z } from 'zod'; -import { useCreateCategorizeFormSchema } from '../../form/categorize-form/use-form-schema'; - -export const useBuildCategorizeHandlePositions = ({ - data, - id, -}: { - id: string; - data: RAGFlowNodeType['data']; -}) => { - const updateNodeInternals = useUpdateNodeInternals(); - - const FormSchema = useCreateCategorizeFormSchema(); - - type FormSchemaType = z.infer<typeof FormSchema>; - - const items: Required<FormSchemaType['items']> = useMemo(() => { - return get(data, `form.items`, []); - }, [data]); - - const positions = useMemo(() => { - const list: Array<{ - top: number; - name: string; - uuid: string; - }> & - Required<FormSchemaType['items']> = []; - - items.forEach((x, idx) => { - list.push({ - ...x, - top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24, - }); - }); - - return list; - }, [items]); - - useEffect(() => { - updateNodeInternals(id); - }, [id, updateNodeInternals, items]); - - return { positions }; -}; diff --git a/web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts b/web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts deleted file mode 100644 index ee221bf82..000000000 --- a/web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ISwitchCondition, RAGFlowNodeType } from '@/interfaces/database/flow'; -import { useUpdateNodeInternals } from '@xyflow/react'; -import get from 'lodash/get'; -import { useEffect, useMemo } from 'react'; -import { SwitchElseTo } from '../../constant'; -import { generateSwitchHandleText } from '../../utils'; - -export const useBuildSwitchHandlePositions = ({ - data, - id, -}: { - id: string; - data: RAGFlowNodeType['data']; -}) => { - const updateNodeInternals = useUpdateNodeInternals(); - - const conditions: ISwitchCondition[] = useMemo(() => { - return get(data, 'form.conditions', []); - }, [data]); - - const positions = useMemo(() => { - const list: Array<{ - text: string; - top: number; - idx: number; - condition?: ISwitchCondition; - }> = []; - - [...conditions, ''].forEach((x, idx) => { - let top = idx === 0 ? 53 : list[idx - 1].top + 10 + 14 + 16 + 16; // case number (Case 1) height + flex gap - if (idx >= 1) { - const previousItems = conditions[idx - 1]?.items ?? []; - if (previousItems.length > 0) { - // top += 12; // ConditionBlock padding - top += previousItems.length * 26; // condition variable height - // top += (previousItems.length - 1) * 25; // operator height - } - } - - list.push({ - text: - idx < conditions.length - ? generateSwitchHandleText(idx) - : SwitchElseTo, - idx, - top, - condition: typeof x === 'string' ? undefined : x, - }); - }); - - return list; - }, [conditions]); - - useEffect(() => { - updateNodeInternals(id); - }, [id, updateNodeInternals, conditions]); - - return { positions }; -}; diff --git a/web/src/pages/data-flow/constant.tsx b/web/src/pages/data-flow/constant.tsx index 85967bb67..950227e60 100644 --- a/web/src/pages/data-flow/constant.tsx +++ b/web/src/pages/data-flow/constant.tsx @@ -1,21 +1,10 @@ -import { - initialKeywordsSimilarityWeightValue, - initialSimilarityThresholdValue, -} from '@/components/similarity-slider'; -import { - AgentGlobals, - CodeTemplateStrMap, - ProgrammingLanguage, -} from '@/constants/agent'; - +import { ParseDocumentType } from '@/components/layout-recognize-form-field'; +import { initialLlmBaseValues } from '@/constants/agent'; import { ChatVariableEnabledField, variableEnabledFieldMap, } from '@/constants/chat'; -import { ModelVariableType } from '@/constants/knowledge'; -import i18n from '@/locales/config'; import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat'; -import { t } from 'i18next'; import { Circle, @@ -28,6 +17,89 @@ import { WrapText, } from 'lucide-react'; +export enum FileType { + PDF = 'pdf', + Spreadsheet = 'spreadsheet', + Image = 'image', + Email = 'email', + TextMarkdown = 'text&markdown', + Docx = 'word', + PowerPoint = 'slides', + Video = 'video', + Audio = 'audio', +} + +export enum PdfOutputFormat { + Json = 'json', + Markdown = 'markdown', +} + +export enum SpreadsheetOutputFormat { + Json = 'json', + Html = 'html', +} + +export enum ImageOutputFormat { + Text = 'text', +} + +export enum EmailOutputFormat { + Json = 'json', + Text = 'text', +} + +export enum TextMarkdownOutputFormat { + Text = 'text', +} + +export enum DocxOutputFormat { + Markdown = 'markdown', + Json = 'json', +} + +export enum PptOutputFormat { + Json = 'json', +} + +export enum VideoOutputFormat { + Json = 'json', +} + +export enum AudioOutputFormat { + Text = 'text', +} + +export const OutputFormatMap = { + [FileType.PDF]: PdfOutputFormat, + [FileType.Spreadsheet]: SpreadsheetOutputFormat, + [FileType.Image]: ImageOutputFormat, + [FileType.Email]: EmailOutputFormat, + [FileType.TextMarkdown]: TextMarkdownOutputFormat, + [FileType.Docx]: DocxOutputFormat, + [FileType.PowerPoint]: PptOutputFormat, + [FileType.Video]: VideoOutputFormat, + [FileType.Audio]: AudioOutputFormat, +}; + +export const InitialOutputFormatMap = { + [FileType.PDF]: PdfOutputFormat.Json, + [FileType.Spreadsheet]: SpreadsheetOutputFormat.Html, + [FileType.Image]: ImageOutputFormat.Text, + [FileType.Email]: EmailOutputFormat.Text, + [FileType.TextMarkdown]: TextMarkdownOutputFormat.Text, + [FileType.Docx]: DocxOutputFormat.Json, + [FileType.PowerPoint]: PptOutputFormat.Json, + [FileType.Video]: VideoOutputFormat.Json, + [FileType.Audio]: AudioOutputFormat.Text, +}; + +export enum ContextGeneratorFieldName { + Summary = 'summary', + Keywords = 'keywords', + Questions = 'questions', + Metadata = 'metadata', +} + export enum PromptRole { User = 'user', Assistant = 'assistant', @@ -38,34 +110,16 @@ export enum AgentDialogueMode { Task = 'task', } -export const BeginId = 'begin'; +export const BeginId = 'File'; export enum Operator { - Begin = 'Begin', - Retrieval = 'Retrieval', - Categorize = 'Categorize', - Message = 'Message', - Relevant = 'Relevant', - RewriteQuestion = 'RewriteQuestion', - KeywordExtract = 'KeywordExtract', - ExeSQL = 'ExeSQL', - Switch = 'Switch', - Concentrator = 'Concentrator', + Begin = 'File', Note = 'Note', - Crawler = 'Crawler', - Invoke = 'Invoke', - Email = 'Email', - Iteration = 'Iteration', - IterationStart = 'IterationItem', - Code = 'CodeExec', - WaitingDialogue = 'WaitingDialogue', - Agent = 'Agent', - Tool = 'Tool', - UserFillUp = 'UserFillUp', - StringTransform = 'StringTransform', Parser = 'Parser', - Chunker = 'Chunker', Tokenizer = 'Tokenizer', + Splitter = 'Splitter', + HierarchicalMerger = 'HierarchicalMerger', + Extractor = 'Extractor', } export const SwitchLogicOperatorOptions = ['and', 'or']; @@ -74,20 +128,6 @@ export const CommonOperatorList = Object.values(Operator).filter( (x) => x !== Operator.Note, ); -export const AgentOperatorList = [ - Operator.Retrieval, - Operator.Categorize, - Operator.Message, - Operator.RewriteQuestion, - Operator.KeywordExtract, - Operator.Switch, - Operator.Concentrator, - Operator.Iteration, - Operator.WaitingDialogue, - Operator.Note, - Operator.Agent, -]; - export const SwitchOperatorOptions = [ { value: '=', label: 'equal', icon: 'equal' }, { value: '≠', label: 'notEqual', icon: 'not-equals' }, @@ -113,38 +153,45 @@ export const SwitchOperatorOptions = [ export const SwitchElseTo = 'end_cpn_ids'; -const initialQueryBaseValues = { - query: [], -}; +export enum TokenizerSearchMethod { + Embedding = 'embedding', + FullText = 'full_text', +} -export const initialRetrievalValues = { - query: AgentGlobals.SysQuery, - top_n: 8, - top_k: 1024, - kb_ids: [], - rerank_id: '', - empty_response: '', - ...initialSimilarityThresholdValue, - ...initialKeywordsSimilarityWeightValue, - use_kg: false, - cross_languages: [], +export enum ImageParseMethod { + OCR = 'ocr', +} + +export enum TokenizerFields { + Text = 'text', + Questions = 'questions', + Summary = 'summary', +} + +export enum ParserFields { + From = 'from', + To = 'to', + Cc = 'cc', + Bcc = 'bcc', + Date = 'date', + Subject = 'subject', + Body = 'body', + Attachments = 'attachments', +} + +export const initialBeginValues = { outputs: { - formalized_content: { + name: { type: 'string', value: '', }, - json: { - type: 'Array<Object>', - value: [], + file: { + type: 'Object', + value: {}, }, }, }; -export const initialBeginValues = { - mode: AgentDialogueMode.Conversational, - prologue: `Hi! I'm your assistant. What can I do for you?`, -}; - export const variableCheckBoxFieldMap = Object.keys( variableEnabledFieldMap, ).reduce<Record<string, boolean>>((pre, cur) => { @@ -154,215 +201,17 @@ export const variableCheckBoxFieldMap = Object.keys( return pre; }, {}); -const initialLlmBaseValues = { - ...variableCheckBoxFieldMap, - temperature: 0.1, - top_p: 0.3, - frequency_penalty: 0.7, - presence_penalty: 0.4, - max_tokens: 256, -}; - -export const initialGenerateValues = { - ...initialLlmBaseValues, - prompt: i18n.t('flow.promptText'), - cite: true, - message_history_window_size: 12, - parameters: [], -}; - -export const initialRewriteQuestionValues = { - ...initialLlmBaseValues, - language: '', - message_history_window_size: 6, -}; - -export const initialRelevantValues = { - ...initialLlmBaseValues, -}; - -export const initialCategorizeValues = { - ...initialLlmBaseValues, - query: AgentGlobals.SysQuery, - parameter: ModelVariableType.Precise, - message_history_window_size: 1, - items: [], - outputs: { - category_name: { - type: 'string', - }, - }, -}; - -export const initialMessageValues = { - content: [''], -}; - -export const initialKeywordExtractValues = { - ...initialLlmBaseValues, - top_n: 3, - ...initialQueryBaseValues, -}; - -export const initialExeSqlValues = { - sql: '', - db_type: 'mysql', - database: '', - username: '', - host: '', - port: 3306, - password: '', - max_records: 1024, - outputs: { - formalized_content: { - value: '', - type: 'string', - }, - json: { - value: [], - type: 'Array<Object>', - }, - }, -}; - -export const initialSwitchValues = { - conditions: [ - { - logical_operator: SwitchLogicOperatorOptions[0], - items: [ - { - operator: SwitchOperatorOptions[0].value, - }, - ], - to: [], - }, - ], - [SwitchElseTo]: [], -}; - -export const initialConcentratorValues = {}; - export const initialNoteValues = { text: '', }; -export const initialCrawlerValues = { - extract_type: 'markdown', - query: '', -}; - -export const initialInvokeValues = { - url: '', - method: 'GET', - timeout: 60, - headers: `{ - "Accept": "*/*", - "Cache-Control": "no-cache", - "Connection": "keep-alive" -}`, - proxy: '', - clean_html: false, - variables: [], - outputs: { - result: { - value: '', - type: 'string', - }, - }, -}; - -export const initialTemplateValues = { - content: '', - parameters: [], -}; - -export const initialEmailValues = { - smtp_server: '', - smtp_port: 465, - email: '', - password: '', - sender_name: '', - to_email: '', - cc_email: '', - subject: '', - content: '', - outputs: { - success: { - value: true, - type: 'boolean', - }, - }, -}; - -export const initialIterationValues = { - items_ref: '', - outputs: {}, -}; -export const initialIterationStartValues = { - outputs: { - item: { - type: 'unkown', - }, - index: { - type: 'integer', - }, - }, -}; - -export const initialCodeValues = { - lang: ProgrammingLanguage.Python, - script: CodeTemplateStrMap[ProgrammingLanguage.Python], - arguments: { - arg1: '', - arg2: '', - }, - outputs: {}, -}; - -export const initialWaitingDialogueValues = {}; - -export const initialChunkerValues = { outputs: {} }; - -export const initialTokenizerValues = {}; - -export const initialAgentValues = { - ...initialLlmBaseValues, - description: '', - user_prompt: '', - sys_prompt: t('flow.sysPromptDefultValue'), - prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }], - message_history_window_size: 12, - max_retries: 3, - delay_after_error: 1, - visual_files_var: '', - max_rounds: 1, - exception_method: '', - exception_goto: [], - exception_default_value: '', - tools: [], - mcp: [], - cite: true, - outputs: { - // structured_output: { - // topic: { - // type: 'string', - // description: - // 'default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.', - // enum: ['general', 'news'], - // default: 'general', - // }, - // }, - content: { - type: 'string', - value: '', - }, - }, -}; - -export const initialUserFillUpValues = { - enable_tips: true, - tips: '', - inputs: [], +export const initialTokenizerValues = { + search_method: [ + TokenizerSearchMethod.Embedding, + TokenizerSearchMethod.FullText, + ], + filename_embd_weight: 0.1, + fields: TokenizerFields.Text, outputs: {}, }; @@ -380,19 +229,84 @@ export enum StringTransformDelimiter { Space = ' ', } -export const initialStringTransformValues = { - method: StringTransformMethod.Merge, - split_ref: '', - script: '', - delimiters: [StringTransformDelimiter.Comma], +export const initialParserValues = { outputs: { - result: { - type: 'string', - }, + markdown: { type: 'string', value: '' }, + text: { type: 'string', value: '' }, + html: { type: 'string', value: '' }, + json: { type: 'Array<object>', value: [] }, }, + setups: [ + { + fileFormat: FileType.PDF, + output_format: PdfOutputFormat.Json, + parse_method: ParseDocumentType.DeepDOC, + }, + { + fileFormat: FileType.Spreadsheet, + output_format: SpreadsheetOutputFormat.Html, + }, + { + fileFormat: FileType.Image, + output_format: ImageOutputFormat.Text, + parse_method: ImageParseMethod.OCR, + system_prompt: '', + }, + { + fileFormat: FileType.Email, + fields: Object.values(ParserFields), + output_format: EmailOutputFormat.Text, + }, + { + fileFormat: FileType.TextMarkdown, + output_format: TextMarkdownOutputFormat.Text, + }, + { + fileFormat: FileType.Docx, + output_format: DocxOutputFormat.Json, + }, + { + fileFormat: FileType.PowerPoint, + output_format: PptOutputFormat.Json, + }, + ], }; -export const initialParserValues = { outputs: {} }; +export const initialSplitterValues = { + outputs: { + chunks: { type: 'Array<Object>', value: [] }, + }, + chunk_token_size: 512, + overlapped_percent: 0, + delimiters: [{ value: '\n' }], +}; + +export enum Hierarchy { + H1 = '1', + H2 = '2', + H3 = '3', + H4 = '4', + H5 = '5', +} + +export const initialHierarchicalMergerValues = { + outputs: { + chunks: { type: 'Array<Object>', value: [] }, + }, + hierarchy: Hierarchy.H3, + levels: [ + { expressions: [{ expression: '^#[^#]' }] }, + { expressions: [{ expression: '^##[^#]' }] }, + { expressions: [{ expression: '^###[^#]' }] }, + { expressions: [{ expression: '^####[^#]' }] }, + ], +}; + +export const initialExtractorValues = { + ...initialLlmBaseValues, + field_name: ContextGeneratorFieldName.Summary, + outputs: {}, +}; export const CategorizeAnchorPointPositions = [ { top: 1, right: 34 }, @@ -411,72 +325,24 @@ export const CategorizeAnchorPointPositions = [ // key is the source of the edge, value is the target of the edge // no connection lines are allowed between key and value -export const RestrictedUpstreamMap = { - [Operator.Begin]: [Operator.Relevant], - [Operator.Categorize]: [Operator.Begin, Operator.Categorize], - [Operator.Retrieval]: [Operator.Begin, Operator.Retrieval], - [Operator.Message]: [ - Operator.Begin, - Operator.Message, - Operator.Retrieval, - Operator.RewriteQuestion, - Operator.Categorize, - ], - [Operator.Relevant]: [Operator.Begin], - [Operator.RewriteQuestion]: [ - Operator.Begin, - Operator.Message, - Operator.RewriteQuestion, - Operator.Relevant, - ], - [Operator.KeywordExtract]: [ - Operator.Begin, - Operator.Message, - Operator.Relevant, - ], - [Operator.ExeSQL]: [Operator.Begin], - [Operator.Switch]: [Operator.Begin], - [Operator.Concentrator]: [Operator.Begin], - [Operator.Crawler]: [Operator.Begin], - [Operator.Note]: [], - [Operator.Invoke]: [Operator.Begin], - [Operator.Email]: [Operator.Begin], - [Operator.Iteration]: [Operator.Begin], - [Operator.IterationStart]: [Operator.Begin], - [Operator.Code]: [Operator.Begin], - [Operator.WaitingDialogue]: [Operator.Begin], - [Operator.Agent]: [Operator.Begin], - [Operator.StringTransform]: [Operator.Begin], - [Operator.UserFillUp]: [Operator.Begin], - [Operator.Tool]: [Operator.Begin], +export const RestrictedUpstreamMap: Record<Operator, Operator[]> = { + [Operator.Begin]: [] as Operator[], + [Operator.Parser]: [Operator.Begin], + [Operator.Splitter]: [Operator.Begin], + [Operator.HierarchicalMerger]: [Operator.Begin], + [Operator.Tokenizer]: [Operator.Begin], + [Operator.Extractor]: [Operator.Begin], + [Operator.Note]: [Operator.Begin], }; export const NodeMap = { [Operator.Begin]: 'beginNode', - [Operator.Categorize]: 'categorizeNode', - [Operator.Retrieval]: 'retrievalNode', - [Operator.Message]: 'messageNode', - [Operator.Relevant]: 'relevantNode', - [Operator.RewriteQuestion]: 'rewriteNode', - [Operator.KeywordExtract]: 'keywordNode', - [Operator.ExeSQL]: 'ragNode', - [Operator.Switch]: 'switchNode', - [Operator.Concentrator]: 'logicNode', [Operator.Note]: 'noteNode', - [Operator.Crawler]: 'ragNode', - [Operator.Invoke]: 'ragNode', - [Operator.Email]: 'ragNode', - [Operator.Iteration]: 'group', - [Operator.IterationStart]: 'iterationStartNode', - [Operator.Code]: 'ragNode', - [Operator.WaitingDialogue]: 'ragNode', - [Operator.Agent]: 'agentNode', - [Operator.Tool]: 'toolNode', - [Operator.UserFillUp]: 'ragNode', - [Operator.StringTransform]: 'ragNode', [Operator.Parser]: 'parserNode', - [Operator.Chunker]: 'chunkerNode', [Operator.Tokenizer]: 'tokenizerNode', + [Operator.Splitter]: 'splitterNode', + [Operator.HierarchicalMerger]: 'hierarchicalMergerNode', + [Operator.Extractor]: 'contextNode', }; export enum BeginQueryType { @@ -497,16 +363,7 @@ export const BeginQueryTypeIconMap = { [BeginQueryType.Boolean]: ToggleLeft, }; -export const NoDebugOperatorsList = [ - Operator.Begin, - Operator.Concentrator, - Operator.Message, - Operator.RewriteQuestion, - Operator.Switch, - Operator.Iteration, - Operator.UserFillUp, - Operator.IterationStart, -]; +export const NoDebugOperatorsList = [Operator.Begin]; export enum NodeHandleId { Start = 'start', @@ -527,3 +384,38 @@ export enum AgentExceptionMethod { Comment = 'comment', Goto = 'goto', } + +export const FileTypeSuffixMap = { + [FileType.PDF]: ['pdf'], + [FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'], + [FileType.Image]: ['jpg', 'jpeg', 'png', 'gif'], + [FileType.Email]: ['eml', 'msg'], + [FileType.TextMarkdown]: ['md', 'markdown', 'mdx', 'txt'], + [FileType.Docx]: ['doc', 'docx'], + [FileType.PowerPoint]: ['pptx'], + [FileType.Video]: [], + [FileType.Audio]: [ + 'da', + 'wave', + 'wav', + 'mp3', + 'aac', + 'flac', + 'ogg', + 'aiff', + 'au', + 'midi', + 'wma', + 'realaudio', + 'vqf', + 'oggvorbis', + 'ape', + ], +}; + +export const SingleOperators = [ + Operator.Tokenizer, + Operator.Splitter, + Operator.HierarchicalMerger, + Operator.Parser, +]; diff --git a/web/src/pages/data-flow/context.ts b/web/src/pages/data-flow/context.ts index 6839554d3..32f2edd86 100644 --- a/web/src/pages/data-flow/context.ts +++ b/web/src/pages/data-flow/context.ts @@ -48,3 +48,11 @@ export type HandleContextType = { export const HandleContext = createContext<HandleContextType>( {} as HandleContextType, ); + +export type LogContextType = { + messageId: string; + setMessageId: (messageId: string) => void; + setUploadedFileData: (data: Record<string, any>) => void; +}; + +export const LogContext = createContext<LogContextType>({} as LogContextType); 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 index f00d93009..d868ab02f 100644 --- a/web/src/pages/data-flow/form-sheet/form-config-map.tsx +++ b/web/src/pages/data-flow/form-sheet/form-config-map.tsx @@ -1,97 +1,30 @@ import { Operator } from '../constant'; -import AgentForm from '../form/agent-form'; -import BeginForm from '../form/begin-form'; -import CategorizeForm from '../form/categorize-form'; -import ChunkerForm from '../form/chunker-form'; -import CodeForm from '../form/code-form'; -import CrawlerForm from '../form/crawler-form'; -import EmailForm from '../form/email-form'; -import ExeSQLForm from '../form/exesql-form'; -import InvokeForm from '../form/invoke-form'; -import IterationForm from '../form/iteration-form'; -import IterationStartForm from '../form/iteration-start-from'; -import KeywordExtractForm from '../form/keyword-extract-form'; -import MessageForm from '../form/message-form'; +import ExtractorForm from '../form/extractor-form'; +import HierarchicalMergerForm from '../form/hierarchical-merger-form'; import ParserForm from '../form/parser-form'; -import RelevantForm from '../form/relevant-form'; -import RetrievalForm from '../form/retrieval-form/next'; -import RewriteQuestionForm from '../form/rewrite-question-form'; -import StringTransformForm from '../form/string-transform-form'; -import SwitchForm from '../form/switch-form'; +import SplitterForm from '../form/splitter-form'; import TokenizerForm from '../form/tokenizer-form'; -import UserFillUpForm from '../form/user-fill-up-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.KeywordExtract]: { - component: KeywordExtractForm, - }, - [Operator.ExeSQL]: { - component: ExeSQLForm, - }, - [Operator.Switch]: { - component: SwitchForm, - }, - [Operator.Crawler]: { - component: CrawlerForm, - }, - [Operator.Invoke]: { - component: InvokeForm, - }, - [Operator.Concentrator]: { component: () => <></>, }, [Operator.Note]: { component: () => <></>, }, - [Operator.Email]: { - component: EmailForm, - }, - [Operator.Iteration]: { - component: IterationForm, - }, - [Operator.IterationStart]: { - component: IterationStartForm, - }, - [Operator.UserFillUp]: { - component: UserFillUpForm, - }, - [Operator.StringTransform]: { - component: StringTransformForm, - }, [Operator.Parser]: { component: ParserForm, }, - [Operator.Chunker]: { - component: ChunkerForm, - }, [Operator.Tokenizer]: { component: TokenizerForm, }, + [Operator.Splitter]: { + component: SplitterForm, + }, + [Operator.HierarchicalMerger]: { + component: HierarchicalMergerForm, + }, + [Operator.Extractor]: { + component: ExtractorForm, + }, }; diff --git a/web/src/pages/data-flow/form-sheet/next.tsx b/web/src/pages/data-flow/form-sheet/next.tsx index 545c08ac4..6e9ce057a 100644 --- a/web/src/pages/data-flow/form-sheet/next.tsx +++ b/web/src/pages/data-flow/form-sheet/next.tsx @@ -8,19 +8,13 @@ import { 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 { X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; 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; @@ -36,13 +30,10 @@ const FormSheet = ({ visible, hideModal, node, - singleDebugDrawerVisible, chatVisible, - hideSingleDebugDrawer, - showSingleDebugDrawer, }: IModalProps<any> & IProps) => { const operatorName: Operator = node?.data.label as Operator; - const clickedToolId = useGraphStore((state) => state.clickedToolId); + // const clickedToolId = useGraphStore((state) => state.clickedToolId); const currentFormMap = FormConfigMap[operatorName]; @@ -53,13 +44,6 @@ const FormSheet = ({ data: node?.data, }); - const isMcp = useMemo(() => { - return ( - operatorName === Operator.Tool && - Object.values(Operator).every((x) => x !== clickedToolId) - ); - }, [clickedToolId, operatorName]); - const { t } = useTranslation(); return ( @@ -75,41 +59,28 @@ const FormSheet = ({ <section className="flex-col border-b py-2 px-5"> <div className="flex items-center gap-2 pb-3"> <OperatorIcon name={operatorName}></OperatorIcon> - - {isMcp ? ( - <div className="flex-1">MCP Config</div> - ) : ( - <div className="flex items-center gap-1 flex-1"> - <label htmlFor="">{t('flow.title')}</label> - {node?.id === BeginId ? ( - <span>{t(BeginId)}</span> - ) : ( - <Input - value={name} - onBlur={handleNameBlur} - onChange={handleNameChange} - ></Input> - )} - </div> - )} - - {needsSingleStepDebugging(operatorName) && ( + <div className="flex items-center gap-1 flex-1"> + <label htmlFor="">{t('flow.title')}</label> + {node?.id === BeginId ? ( + <span>{t(BeginId)}</span> + ) : ( + <Input + value={name} + onBlur={handleNameBlur} + onChange={handleNameChange} + ></Input> + )} + </div> + {/* {needsSingleStepDebugging(operatorName) && ( <RunTooltip> <Play className="size-5 cursor-pointer" onClick={showSingleDebugDrawer} /> </RunTooltip> - )} + )} */} <X onClick={hideModal} /> </div> - {isMcp || ( - <span> - {t( - `dataflow.${lowerFirst(operatorName === Operator.Tool ? clickedToolId : operatorName)}Description`, - )} - </span> - )} </section> </SheetHeader> <section className="pt-4 overflow-auto flex-1"> @@ -120,13 +91,6 @@ const FormSheet = ({ )} </section> </SheetContent> - {singleDebugDrawerVisible && ( - <SingleDebugSheet - visible={singleDebugDrawerVisible} - hideModal={hideSingleDebugDrawer} - componentId={node?.id} - ></SingleDebugSheet> - )} </Sheet> ); }; 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 deleted file mode 100644 index c5fe6e876..000000000 --- a/web/src/pages/data-flow/form-sheet/single-debug-sheet/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -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<any> & 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 ( - <Sheet open={visible} modal={false}> - <SheetContent className="top-20 p-0" closeIcon={false}> - <SheetHeader className="py-2 px-5"> - <div className="flex justify-between "> - {t('flow.testRun')} - <X onClick={hideModal} className="cursor-pointer" /> - </div> - </SheetHeader> - <section className="overflow-y-auto pt-4 px-5"> - <DebugContent - parameters={list} - ok={onOk} - isNext={false} - loading={loading} - submitButtonDisabled={list.length === 0} - ></DebugContent> - {!isEmpty(data) ? ( - <div - className={cn('mt-4 rounded-md border', { - [`border-state-error`]: !isEmpty(data._ERROR), - })} - > - <div className="flex justify-between p-2"> - <span>JSON</span> - <CopyToClipboard text={content}></CopyToClipboard> - </div> - <JsonView - src={data} - displaySize - collapseStringsAfterLength={100000000000} - className="w-full h-[800px] break-words overflow-auto p-2" - dark - /> - </div> - ) : null} - </section> - </SheetContent> - </Sheet> - ); -}; - -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 deleted file mode 100644 index 9e5620823..000000000 --- a/web/src/pages/data-flow/form/agent-form/agent-tools.tsx +++ /dev/null @@ -1,191 +0,0 @@ -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<HTMLLIElement>) { - const element = useMemo(() => { - return ( - <li - {...props} - className={cn( - 'flex bg-bg-card p-1 rounded-sm justify-between', - className, - )} - > - {children} - </li> - ); - }, [children, className, props]); - - if (children === Operator.Code) { - return ( - <Tooltip> - <TooltipTrigger asChild>{element}</TooltipTrigger> - <TooltipContent> - <p>It doesn't have any config.</p> - </TooltipContent> - </Tooltip> - ); - } - - return element; -} - -type ActionButtonProps<T> = { - record: T; - deleteRecord(record: T): void; - edit: MouseEventHandler<HTMLOrSVGElement>; -}; - -function ActionButton<T>({ deleteRecord, record, edit }: ActionButtonProps<T>) { - const handleDelete = useCallback(() => { - deleteRecord(record); - }, [deleteRecord, record]); - - return ( - <div className="flex items-center gap-2 text-text-secondary"> - <PencilLine - className="size-4 cursor-pointer" - data-tool={record} - onClick={edit} - /> - <X className="size-4 cursor-pointer" onClick={handleDelete} /> - </div> - ); -} - -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<SVGSVGElement> = useCallback( - (e) => { - const toolNodeId = findAgentToolNodeById(clickedNodeId); - if (toolNodeId) { - selectNodeIds([toolNodeId]); - showFormDrawer(e, toolNodeId); - } - }, - [clickedNodeId, findAgentToolNodeById, selectNodeIds, showFormDrawer], - ); - - return ( - <section className="space-y-2.5"> - <span className="text-text-secondary">{t('flow.tools')}</span> - <ul className="space-y-2"> - {toolNames.map((x) => ( - <ToolCard key={x}> - <div className="flex gap-2 items-center"> - <OperatorIcon name={x as Operator}></OperatorIcon> - {x} - </div> - <ActionButton - record={x} - deleteRecord={deleteNodeTool(x)} - edit={handleEdit} - ></ActionButton> - </ToolCard> - ))} - {mcpIds.map((id) => ( - <ToolCard key={id}> - {findMcpById(id)?.name} - <ActionButton - record={id} - deleteRecord={deleteNodeMCP(id)} - edit={handleEdit} - ></ActionButton> - </ToolCard> - ))} - </ul> - <ToolPopover> - <BlockButton>{t('flow.addTools')}</BlockButton> - </ToolPopover> - </section> - ); -} - -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<SVGSVGElement> => - (e) => { - selectNodeIds([nodeId]); - showFormDrawer(e, nodeId); - }, - [selectNodeIds, showFormDrawer], - ); - - const subBottomAgentNodeIds = useMemo(() => { - return filterDownstreamAgentNodeIds(edges, node?.id); - }, [edges, node?.id]); - - return ( - <section className="space-y-2.5"> - <span className="text-text-secondary">{t('flow.agent')}</span> - <ul className="space-y-2"> - {subBottomAgentNodeIds.map((id) => { - const currentNode = getNode(id); - - return ( - <ToolCard key={id}> - {currentNode?.data.name} - <ActionButton - record={id} - deleteRecord={deleteAgentDownstreamNodesById} - edit={handleEdit(id)} - ></ActionButton> - </ToolCard> - ); - })} - </ul> - <BlockButton - onClick={addCanvasNode(Operator.Agent, { - nodeId: node?.id, - position: Position.Bottom, - })} - > - {t('flow.addAgent')} - </BlockButton> - </section> - ); -} 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 deleted file mode 100644 index 1cda9fbd5..000000000 --- a/web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx +++ /dev/null @@ -1,93 +0,0 @@ -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 ( - <FormItem> - <FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel> - <div className="space-y-4"> - {fields.map((field, index) => ( - <div key={field.id} className="flex"> - <div className="space-y-2 flex-1"> - <FormField - control={form.control} - name={`${name}.${index}.role`} - render={({ field }) => ( - <FormItem className="w-1/3"> - <FormLabel /> - <FormControl> - <RAGFlowSelect - {...field} - options={options} - ></RAGFlowSelect> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name={`${name}.${index}.content`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormControl> - <section> - <PromptEditor - {...field} - showToolbar={false} - ></PromptEditor> - </section> - </FormControl> - </FormItem> - )} - /> - </div> - <Button - type="button" - variant={'ghost'} - onClick={() => remove(index)} - > - <X /> - </Button> - </div> - ))} - </div> - <FormMessage /> - <BlockButton - onClick={() => append({ content: '', role: PromptRole.User })} - > - Add - </BlockButton> - </FormItem> - ); -}; - -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 deleted file mode 100644 index afda465b6..000000000 --- a/web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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 ( - <FormItem> - <div className="space-y-4"> - {fields.map((field, index) => ( - <div key={field.id} className="flex"> - <div className="space-y-2 flex-1"> - <FormField - control={form.control} - name={`${name}.${index}.component_name`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormControl> - <section> - <PromptEditor - {...field} - showToolbar={false} - ></PromptEditor> - </section> - </FormControl> - </FormItem> - )} - /> - </div> - <Button - type="button" - variant={'ghost'} - onClick={() => remove(index)} - > - <X /> - </Button> - </div> - ))} - </div> - <FormMessage /> - <BlockButton onClick={() => append({ component_name: '' })}> - Add - </BlockButton> - </FormItem> - ); -}; - -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 deleted file mode 100644 index 9ca0fb69c..000000000 --- a/web/src/pages/data-flow/form/agent-form/index.tsx +++ /dev/null @@ -1,280 +0,0 @@ -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<z.infer<typeof FormSchema>>({ - 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 ( - <Form {...form}> - <FormWrapper> - <FormContainer> - {isSubAgent && <DescriptionField></DescriptionField>} - <LargeModelFormField showSpeech2TextModel></LargeModelFormField> - {findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && ( - <QueryVariable - name="visual_files_var" - label="Visual Input File" - type={VariableType.File} - ></QueryVariable> - )} - </FormContainer> - - <FormContainer> - <FormField - control={form.control} - name={`sys_prompt`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.systemPrompt')}</FormLabel> - <FormControl> - <PromptEditor - {...field} - placeholder={t('flow.messagePlaceholder')} - showToolbar={false} - ></PromptEditor> - </FormControl> - </FormItem> - )} - /> - </FormContainer> - {isSubAgent || ( - <FormContainer> - {/* <DynamicPrompt></DynamicPrompt> */} - <FormField - control={form.control} - name={`prompts`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.userPrompt')}</FormLabel> - <FormControl> - <section> - <PromptEditor - {...field} - showToolbar={false} - ></PromptEditor> - </section> - </FormControl> - </FormItem> - )} - /> - </FormContainer> - )} - - <FormContainer> - <AgentTools></AgentTools> - <Agents node={node}></Agents> - </FormContainer> - <Collapse title={<div>{t('flow.advancedSettings')}</div>}> - <FormContainer> - <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> - <FormField - control={form.control} - name={`cite`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel tooltip={t('flow.citeTip')}> - {t('flow.cite')} - </FormLabel> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - ></Switch> - </FormControl> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`max_retries`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.maxRetries')}</FormLabel> - <FormControl> - <NumberInput {...field} max={8}></NumberInput> - </FormControl> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`delay_after_error`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.delayEfterError')}</FormLabel> - <FormControl> - <NumberInput {...field} max={5} step={0.1}></NumberInput> - </FormControl> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`max_rounds`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.maxRounds')}</FormLabel> - <FormControl> - <NumberInput {...field}></NumberInput> - </FormControl> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`exception_method`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.exceptionMethod')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={ExceptionMethodOptions} - allowClear - /> - </FormControl> - </FormItem> - )} - /> - {exceptionMethod === AgentExceptionMethod.Comment && ( - <FormField - control={form.control} - name={`exception_default_value`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.ExceptionDefaultValue')}</FormLabel> - <FormControl> - <Input {...field} /> - </FormControl> - </FormItem> - )} - /> - )} - </FormContainer> - </Collapse> - <Output list={outputList}></Output> - </FormWrapper> - </Form> - ); -} - -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 deleted file mode 100644 index 84f1ed46a..000000000 --- a/web/src/pages/data-flow/form/agent-form/tool-popover/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -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 ( - <Popover> - <PopoverTrigger asChild>{children}</PopoverTrigger> - <PopoverContent className="w-80 p-4"> - <Tabs defaultValue={ToolType.Common}> - <TabsList> - <TabsTrigger value={ToolType.Common} className="bg-bg-card"> - {t('flow.builtIn')} - </TabsTrigger> - <TabsTrigger value={ToolType.MCP} className="bg-bg-card"> - MCP - </TabsTrigger> - </TabsList> - <TabsContent value={ToolType.Common}> - <ToolCommand - onChange={handleChange} - value={toolNames} - ></ToolCommand> - </TabsContent> - <TabsContent value={ToolType.MCP}> - <MCPCommand value={mcpIds} onChange={updateNodeMCP}></MCPCommand> - </TabsContent> - </Tabs> - </PopoverContent> - </Popover> - ); -} 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 deleted file mode 100644 index 6118c43ab..000000000 --- a/web/src/pages/data-flow/form/agent-form/tool-popover/tool-command.tsx +++ /dev/null @@ -1,178 +0,0 @@ -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 ( - <CommandItem className="cursor-pointer" onSelect={() => toggleOption(id)}> - <div - className={cn( - 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', - isSelected - ? 'bg-primary text-primary-foreground' - : 'opacity-50 [&_svg]:invisible', - )} - > - <CheckIcon className="h-4 w-4" /> - </div> - {children} - </CommandItem> - ); -} - -function useHandleSelectChange({ onChange, value }: ToolCommandProps) { - const [currentValue, setCurrentValue] = useState<string[]>([]); - - 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 ( - <Command> - <CommandInput placeholder={t('flow.typeCommandOrsearch')} /> - <CommandList> - <CommandEmpty>No results found.</CommandEmpty> - {Menus.map((x) => ( - <CommandGroup heading={x.label} key={x.label}> - {x.list.map((y) => { - const isSelected = currentValue.includes(y); - return ( - <ToolCommandItem - key={y} - id={y} - toggleOption={toggleOption} - isSelected={isSelected} - > - <> - <OperatorIcon name={y as Operator}></OperatorIcon> - <span>{t(`flow.${lowerFirst(y)}`)}</span> - </> - </ToolCommandItem> - ); - })} - </CommandGroup> - ))} - </CommandList> - </Command> - ); -} - -export function MCPCommand({ onChange, value }: ToolCommandProps) { - const { data } = useListMcpServer(); - const { toggleOption, currentValue } = useHandleSelectChange({ - onChange, - value, - }); - - return ( - <Command> - <CommandInput placeholder="Type a command or search..." /> - <CommandList> - <CommandEmpty>No results found.</CommandEmpty> - {data.mcp_servers.map((item) => { - const isSelected = currentValue.includes(item.id); - - return ( - <ToolCommandItem - key={item.id} - id={item.id} - isSelected={isSelected} - toggleOption={toggleOption} - > - {item.name} - </ToolCommandItem> - ); - })} - </CommandList> - </Command> - ); -} 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 deleted file mode 100644 index 827b9f9e1..000000000 --- a/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-mcp.ts +++ /dev/null @@ -1,74 +0,0 @@ -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<IAgentForm['mcp']>((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 deleted file mode 100644 index db579561a..000000000 --- a/web/src/pages/data-flow/form/agent-form/tool-popover/use-update-tools.ts +++ /dev/null @@ -1,66 +0,0 @@ -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<IAgentForm['tools']>((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 deleted file mode 100644 index 32bf3f0ef..000000000 --- a/web/src/pages/data-flow/form/agent-form/use-get-tools.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index b2d61dc9f..000000000 --- a/web/src/pages/data-flow/form/agent-form/use-values.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 98b0ecf31..000000000 --- a/web/src/pages/data-flow/form/agent-form/use-watch-change.ts +++ /dev/null @@ -1,22 +0,0 @@ -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<any>) { - 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/begin-form/begin-dynamic-options.tsx b/web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx deleted file mode 100644 index 12b2bfb4b..000000000 --- a/web/src/pages/data-flow/form/begin-form/begin-dynamic-options.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'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 ( - <div className="space-y-5"> - {fields.map((field, index) => { - const typeField = `${name}.${index}.value`; - return ( - <div key={field.id} className="flex items-center gap-2"> - <FormField - control={form.control} - name={typeField} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormControl> - <Input - {...field} - placeholder={t('common.pleaseInput')} - ></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <Button variant={'ghost'} onClick={() => remove(index)}> - <X className="text-text-sub-title-invert " /> - </Button> - </div> - ); - })} - <BlockButton onClick={() => append({ value: '' })} type="button"> - {t('flow.addField')} - </BlockButton> - </div> - ); -} diff --git a/web/src/pages/data-flow/form/begin-form/index.tsx b/web/src/pages/data-flow/form/begin-form/index.tsx deleted file mode 100644 index ad4eb9d3e..000000000 --- a/web/src/pages/data-flow/form/begin-form/index.tsx +++ /dev/null @@ -1,205 +0,0 @@ -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 ( - <section className="px-5 space-y-5"> - <Form {...form}> - <FormField - control={form.control} - name={'mode'} - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('flow.modeTip')}> - {t('flow.mode')} - </FormLabel> - <FormControl> - <RAGFlowSelect - placeholder={t('common.pleaseSelect')} - options={ModeOptions} - {...field} - ></RAGFlowSelect> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - {mode === AgentDialogueMode.Conversational && ( - <FormField - control={form.control} - name={'enablePrologue'} - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('flow.openingSwitchTip')}> - {t('flow.openingSwitch')} - </FormLabel> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - )} - {mode === AgentDialogueMode.Conversational && enablePrologue && ( - <FormField - control={form.control} - name={'prologue'} - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('chat.setAnOpenerTip')}> - {t('flow.openingCopy')} - </FormLabel> - <FormControl> - <Textarea - rows={5} - {...field} - placeholder={t('common.pleaseInput')} - ></Textarea> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - )} - {/* Create a hidden field to make Form instance record this */} - <FormField - control={form.control} - name={'inputs'} - render={() => <div></div>} - /> - <Collapse - title={ - <div> - {t('flow.input')} - <FormTooltip tooltip={t('flow.beginInputTip')}></FormTooltip> - </div> - } - rightContent={ - <Button - variant={'ghost'} - onClick={(e) => { - e.preventDefault(); - showModal(); - }} - > - <Plus /> - </Button> - } - > - <QueryTable - data={inputs} - showModal={showModal} - deleteRecord={handleDeleteRecord} - ></QueryTable> - </Collapse> - {visible && ( - <ParameterDialog - hideModal={hideModal} - initialValue={currentRecord} - otherThanCurrentQuery={otherThanCurrentQuery} - submit={ok} - ></ParameterDialog> - )} - </Form> - </section> - ); -} - -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 deleted file mode 100644 index 3b9070437..000000000 --- a/web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx +++ /dev/null @@ -1,226 +0,0 @@ -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<z.infer<typeof FormSchema>>({ - resolver: zodResolver(FormSchema), - mode: 'onChange', - defaultValues: { - type: BeginQueryType.Line, - optional: false, - key: '', - name: '', - options: [], - }, - }); - - const options = useMemo(() => { - return Object.values(BeginQueryType).reduce<RAGFlowSelectOptionType[]>( - (pre, cur) => { - const Icon = BeginQueryTypeIconMap[cur]; - - return [ - ...pre, - { - label: ( - <div className="flex items-center gap-2"> - <Icon - className={`size-${cur === BeginQueryType.Options ? 4 : 5}`} - ></Icon> - {t(cur.toLowerCase())} - </div> - ), - 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<typeof FormSchema>) { - const values = { ...data, options: data.options?.map((x) => x.value) }; - console.log('🚀 ~ onSubmit ~ values:', values); - - submit(values); - } - - const handleKeyChange = (e: ChangeEvent<HTMLInputElement>) => { - const name = form.getValues().name || ''; - form.setValue('key', e.target.value.trim()); - if (!name) { - form.setValue('name', e.target.value.trim()); - } - }; - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - id={FormId} - className="space-y-5" - autoComplete="off" - > - <FormField - name="type" - control={form.control} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('type')}</FormLabel> - <FormControl> - <RAGFlowSelect {...field} options={options} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - name="key" - control={form.control} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('key')}</FormLabel> - <FormControl> - <Input {...field} autoComplete="off" onBlur={handleKeyChange} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - name="name" - control={form.control} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('name')}</FormLabel> - <FormControl> - <Input {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - name="optional" - control={form.control} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('optional')}</FormLabel> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - {type === BeginQueryType.Options && ( - <BeginDynamicOptions></BeginDynamicOptions> - )} - </form> - </Form> - ); -} - -export function ParameterDialog({ - initialValue, - hideModal, - otherThanCurrentQuery, - submit, -}: ModalFormProps & IModalProps<BeginQuery>) { - const { t } = useTranslation(); - - return ( - <Dialog open onOpenChange={hideModal}> - <DialogContent> - <DialogHeader> - <DialogTitle>{t('flow.variableSettings')}</DialogTitle> - </DialogHeader> - <ParameterForm - initialValue={initialValue} - otherThanCurrentQuery={otherThanCurrentQuery} - submit={submit} - ></ParameterForm> - <DialogFooter> - <Button type="submit" form={FormId}> - {t('modal.okText')} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} 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 deleted file mode 100644 index 5701c49b1..000000000 --- a/web/src/pages/data-flow/form/begin-form/query-table.tsx +++ /dev/null @@ -1,199 +0,0 @@ -'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<SortingState>([]); - const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState<VisibilityState>({}); - - const columns: ColumnDef<BeginQuery>[] = [ - { - accessorKey: 'key', - header: t('flow.key'), - meta: { cellClassName: 'max-w-30' }, - cell: ({ row }) => { - const key: string = row.getValue('key'); - return ( - <Tooltip> - <TooltipTrigger asChild> - <div className="truncate ">{key}</div> - </TooltipTrigger> - <TooltipContent> - <p>{key}</p> - </TooltipContent> - </Tooltip> - ); - }, - }, - { - accessorKey: 'name', - header: t('flow.name'), - meta: { cellClassName: 'max-w-30' }, - cell: ({ row }) => { - const name: string = row.getValue('name'); - return ( - <Tooltip> - <TooltipTrigger asChild> - <div className="truncate">{name}</div> - </TooltipTrigger> - <TooltipContent> - <p>{name}</p> - </TooltipContent> - </Tooltip> - ); - }, - }, - { - accessorKey: 'type', - header: t('flow.type'), - cell: ({ row }) => ( - <div> - {t(`flow.${(row.getValue('type')?.toString() || '').toLowerCase()}`)} - </div> - ), - }, - { - accessorKey: 'optional', - header: t('flow.optional'), - cell: ({ row }) => <div>{row.getValue('optional') ? 'Yes' : 'No'}</div>, - }, - { - id: 'actions', - enableHiding: false, - header: t('common.action'), - cell: ({ row }) => { - const record = row.original; - const idx = row.index; - - return ( - <div> - <Button - className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground" - onClick={() => showModal(idx, record)} - > - <Pencil /> - </Button> - <Button - className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground" - onClick={() => deleteRecord(idx)} - > - <Trash2 /> - </Button> - </div> - ); - }, - }, - ]; - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - state: { - sorting, - columnFilters, - columnVisibility, - }, - }); - - return ( - <div className="w-full"> - <div className="rounded-md border"> - <Table rootClassName="rounded-md"> - <TableHeader> - {table.getHeaderGroups().map((headerGroup) => ( - <TableRow key={headerGroup.id}> - {headerGroup.headers.map((header) => { - return ( - <TableHead key={header.id}> - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - </TableHead> - ); - })} - </TableRow> - ))} - </TableHeader> - <TableBody> - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - <TableRow - key={row.id} - data-state={row.getIsSelected() && 'selected'} - > - {row.getVisibleCells().map((cell) => ( - <TableCell - key={cell.id} - className={cn(cell.column.columnDef.meta?.cellClassName)} - > - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - </TableCell> - ))} - </TableRow> - )) - ) : ( - <TableEmpty columnsLength={columns.length}></TableEmpty> - )} - </TableBody> - </Table> - </div> - </div> - ); -} 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 deleted file mode 100644 index 6942ba88b..000000000 --- a/web/src/pages/data-flow/form/begin-form/use-edit-query.ts +++ /dev/null @@ -1,67 +0,0 @@ -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<BeginQuery>(); - 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 deleted file mode 100644 index 10326bae8..000000000 --- a/web/src/pages/data-flow/form/begin-form/use-values.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index f0da58068..000000000 --- a/web/src/pages/data-flow/form/begin-form/use-watch-change.ts +++ /dev/null @@ -1,31 +0,0 @@ -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<Record<string, Omit<BeginQuery, 'key'>>>((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 deleted file mode 100644 index 36038c4f6..000000000 --- a/web/src/pages/data-flow/form/begin-form/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BeginQuery } from '../../interface'; - -export function buildBeginInputListFromObject( - inputs: Record<string, Omit<BeginQuery, 'key'>>, -) { - return Object.entries(inputs || {}).reduce<BeginQuery[]>( - (pre, [key, value]) => { - pre.push({ ...(value || {}), key }); - - return pre; - }, - [], - ); -} 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 deleted file mode 100644 index 0807f7bfa..000000000 --- a/web/src/pages/data-flow/form/categorize-form/dynamic-categorize.tsx +++ /dev/null @@ -1,249 +0,0 @@ -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<string | undefined>(); - const { t } = useTranslate('flow'); - - const handleNameChange: ChangeEventHandler<HTMLInputElement> = 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<HTMLInputElement> = useCallback( - (e) => { - const val = e.target.value; - if (otherNames?.every((x) => x !== val) && trim(val) !== '') { - onChange?.(val); - } - }, - [onChange, otherNames], - ); - - useEffect(() => { - setName(value); - }, [value]); - - return ( - <Input - value={name} - onChange={handleNameChange} - onBlur={handleNameBlur} - ></Input> - ); -}; - -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 ( - <section className="space-y-4"> - <FormField - control={form.control} - name={buildFieldName('name')} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('categoryName')}</FormLabel> - <FormControl> - <NameInput - {...field} - otherNames={getOtherFieldValues(form, 'items', index, 'name')} - validate={(error?: string) => { - const fieldName = buildFieldName('name'); - if (error) { - form.setError(fieldName, { message: error }); - } else { - form.clearErrors(fieldName); - } - }} - ></NameInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={buildFieldName('description')} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('description')}</FormLabel> - <FormControl> - <BlurTextarea {...field} rows={3} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - {/* Create a hidden field to make Form instance record this */} - <FormField - control={form.control} - name={'uuid'} - render={() => <div></div>} - /> - <DynamicExample name={buildFieldName('examples')}></DynamicExample> - </section> - ); -}; - -const FormSet = memo(InnerFormSet); - -const DynamicCategorize = ({ nodeId }: IProps) => { - const updateNodeInternals = useUpdateNodeInternals(); - const FormSchema = useCreateCategorizeFormSchema(); - - const deleteCategorizeCaseEdges = useGraphStore( - (state) => state.deleteEdgesBySourceAndSourceHandle, - ); - const form = useFormContext<z.infer<typeof FormSchema>>(); - 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 ( - <div className="flex flex-col gap-4 "> - {fields.map((field, index) => ( - <Collapsible key={field.id} defaultOpen> - <div className="flex items-center justify-between space-x-4"> - <h4 className="font-bold"> - {form.getValues(`items.${index}.name`)} - </h4> - <CollapsibleTrigger asChild> - <div className="flex gap-4"> - <Button - variant="ghost" - size="sm" - className="w-9 p-0" - onClick={handleRemove(index)} - > - <X className="h-4 w-4" /> - </Button> - <Button variant="ghost" size="sm" className="w-9 p-0"> - <ChevronsUpDown className="h-4 w-4" /> - <span className="sr-only">Toggle</span> - </Button> - </div> - </CollapsibleTrigger> - </div> - <CollapsibleContent> - <FormSet nodeId={nodeId} index={index}></FormSet> - </CollapsibleContent> - </Collapsible> - ))} - - <Button type={'button'} onClick={handleAdd}> - <PlusOutlined /> - {t('addCategory')} - </Button> - </div> - ); -}; - -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 deleted file mode 100644 index 35d95cbc6..000000000 --- a/web/src/pages/data-flow/form/categorize-form/dynamic-example.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 ( - <FormItem> - <FormLabel tooltip={t('flow.msgTip')}>{t('flow.examples')}</FormLabel> - <div className="space-y-4"> - {fields.map((field, index) => ( - <div key={field.id} className="flex items-start gap-2"> - <FormField - control={form.control} - name={`${name}.${index}.value`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormControl> - <Textarea {...field}> </Textarea> - </FormControl> - </FormItem> - )} - /> - {index === 0 ? ( - <Button - type="button" - variant={'ghost'} - onClick={() => append({ value: '' })} - > - <Plus /> - </Button> - ) : ( - <Button - type="button" - variant={'ghost'} - onClick={() => remove(index)} - > - <X /> - </Button> - )} - </div> - ))} - </div> - <FormMessage /> - </FormItem> - ); -}; - -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 deleted file mode 100644 index c36ff4529..000000000 --- a/web/src/pages/data-flow/form/categorize-form/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -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 ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <QueryVariable></QueryVariable> - <LargeModelFormField></LargeModelFormField> - </FormContainer> - <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> - <DynamicCategorize nodeId={node?.id}></DynamicCategorize> - <Output list={outputList}></Output> - </FormWrapper> - </Form> - ); -} - -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 deleted file mode 100644 index 9e56bb18b..000000000 --- a/web/src/pages/data-flow/form/categorize-form/use-form-schema.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index a920ec4cc..000000000 --- a/web/src/pages/data-flow/form/categorize-form/use-values.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index a97b80a77..000000000 --- a/web/src/pages/data-flow/form/categorize-form/use-watch-change.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from 'react'; -import { UseFormReturn, useWatch } from 'react-hook-form'; -import useGraphStore from '../../store'; - -export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { - 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/chunker-form/index.tsx b/web/src/pages/data-flow/form/chunker-form/index.tsx deleted file mode 100644 index cfc32e82f..000000000 --- a/web/src/pages/data-flow/form/chunker-form/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -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 { memo } from 'react'; -import { useForm, useFormContext } from 'react-hook-form'; -import { z } from 'zod'; -import { initialChunkerValues } 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(initialChunkerValues.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 ( - <> - <FormField - control={form.control} - name={`country`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('country')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={GoogleCountryOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`language`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('language')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={GoogleLanguageOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </> - ); -} - -const ChunkerForm = ({ node }: INextOperatorForm) => { - const { t } = useTranslate('flow'); - const defaultValues = useFormValues(initialChunkerValues, node); - - const form = useForm<z.infer<typeof FormSchema>>({ - defaultValues, - resolver: zodResolver(FormSchema), - }); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <QueryVariable name="q"></QueryVariable> - </FormContainer> - <FormContainer> - <ApiKeyField placeholder={t('apiKeyPlaceholder')}></ApiKeyField> - <FormField - control={form.control} - name={`start`} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flowStart')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`num`} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flowNum')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <GoogleFormWidgets></GoogleFormWidgets> - </FormContainer> - </FormWrapper> - <div className="p-5"> - <Output list={outputList}></Output> - </div> - </Form> - ); -}; - -export default memo(ChunkerForm); diff --git a/web/src/pages/data-flow/form/code-form/index.tsx b/web/src/pages/data-flow/form/code-form/index.tsx deleted file mode 100644 index 2883fdf46..000000000 --- a/web/src/pages/data-flow/form/code-form/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -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<FormSchemaType>({ - defaultValues: values, - resolver: zodResolver(FormSchema), - }); - - useWatchFormChange(node?.id, form); - - const handleLanguageChange = useHandleLanguageChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper> - <DynamicInputVariable - node={node} - title={t('flow.input')} - isOutputs={false} - ></DynamicInputVariable> - <FormField - control={form.control} - name="script" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center justify-between"> - Code - <FormField - control={form.control} - name="lang" - render={({ field }) => ( - <FormItem> - <FormControl> - <RAGFlowSelect - {...field} - onChange={(val) => { - field.onChange(val); - handleLanguageChange(val); - }} - options={options} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </FormLabel> - <FormControl> - <Editor - height={300} - theme={isDarkTheme ? 'vs-dark' : 'vs'} - language={formData.lang} - options={{ - minimap: { enabled: false }, - automaticLayout: true, - }} - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {formData.lang === ProgrammingLanguage.Python ? ( - <DynamicInputVariable - node={node} - title={'Return Values'} - name={DynamicFieldName} - isOutputs - ></DynamicInputVariable> - ) : ( - <div> - <VariableTitle title={'Return Values'}></VariableTitle> - <FormContainer className="space-y-5"> - <FormField - control={form.control} - name={`${DynamicFieldName}.name`} - render={({ field }) => ( - <FormItem> - <FormLabel>Name</FormLabel> - <FormControl> - <Input - {...field} - placeholder={t('common.pleaseInput')} - ></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`${DynamicFieldName}.type`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>Type</FormLabel> - <FormControl> - <RAGFlowSelect - placeholder={t('common.pleaseSelect')} - options={TypeOptions} - {...field} - ></RAGFlowSelect> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </FormContainer> - </div> - )} - </FormWrapper> - <div className="p-5"> - <Output list={buildOutputList(formData.outputs)}></Output> - </div> - </Form> - ); -} - -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 deleted file mode 100644 index 39a2dd4a4..000000000 --- a/web/src/pages/data-flow/form/code-form/next-variable.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'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<String>', - 'Array<Number>', - '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 ( - <div className="space-y-5"> - {fields.map((field, index) => { - const typeField = `${name}.${index}.name`; - return ( - <div key={field.id} className="flex w-full items-center gap-2"> - <FormField - control={form.control} - name={typeField} - render={({ field }) => ( - <FormItem className="flex-1 overflow-hidden"> - <FormControl> - <BlurInput - {...field} - placeholder={t('common.pleaseInput')} - ></BlurInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <Separator className="w-3 text-text-secondary" /> - <FormField - control={form.control} - name={`${name}.${index}.type`} - render={({ field }) => ( - <FormItem className="flex-1 overflow-hidden"> - <FormControl> - {isOutputs ? ( - <RAGFlowSelect - placeholder={t('common.pleaseSelect')} - options={TypeOptions} - {...field} - ></RAGFlowSelect> - ) : ( - <SelectWithSearch - options={nextOptions} - {...field} - ></SelectWithSearch> - )} - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <Button variant={'ghost'} onClick={() => remove(index)}> - <X className="text-text-sub-title-invert " /> - </Button> - </div> - ); - })} - <BlockButton onClick={() => append({ name: '', type: undefined })}> - {t('flow.addVariable')} - </BlockButton> - </div> - ); -} - -export function VariableTitle({ title }: { title: ReactNode }) { - return <div className="font-medium text-text-primary pb-2">{title}</div>; -} - -export function DynamicInputVariable({ - node, - name, - title, - isOutputs = false, -}: IProps & { title: ReactNode }) { - return ( - <section> - <VariableTitle title={title}></VariableTitle> - <FormContainer> - <DynamicVariableForm - node={node} - name={name} - isOutputs={isOutputs} - ></DynamicVariableForm> - </FormContainer> - </section> - ); -} diff --git a/web/src/pages/data-flow/form/code-form/schema.ts b/web/src/pages/data-flow/form/code-form/schema.ts deleted file mode 100644 index fe694444e..000000000 --- a/web/src/pages/data-flow/form/code-form/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -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<typeof FormSchema>; 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 deleted file mode 100644 index ea6f2d67c..000000000 --- a/web/src/pages/data-flow/form/code-form/use-values.ts +++ /dev/null @@ -1,47 +0,0 @@ -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<string, string>) { - 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<OutputsFormType>((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 deleted file mode 100644 index 80e0c8b15..000000000 --- a/web/src/pages/data-flow/form/code-form/use-watch-change.ts +++ /dev/null @@ -1,95 +0,0 @@ -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<Record<string, string>>((pre, cur) => { - pre[cur.name] = cur.type; - - return pre; - }, {}); -} - -type ArrayOutputs = Extract<FormSchemaType['outputs'], Array<any>>; - -type ObjectOutputs = Exclude<FormSchemaType['outputs'], Array<any>>; - -function convertOutputsToObject({ lang, outputs }: FormSchemaType) { - if (lang === ProgrammingLanguage.Python) { - return (outputs as ArrayOutputs).reduce<ICodeForm['outputs']>( - (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<FormSchemaType>, -) { - 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<FormSchemaType>, -) { - 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 deleted file mode 100644 index f9debfc4f..000000000 --- a/web/src/pages/data-flow/form/components/api-key-field.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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 ( - <FormField - control={form.control} - name="api_key" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.apiKey')}</FormLabel> - <FormControl> - <Input type="password" {...field} placeholder={placeholder}></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} diff --git a/web/src/pages/data-flow/form/components/description-field.tsx b/web/src/pages/data-flow/form/components/description-field.tsx deleted file mode 100644 index 8fa2eef64..000000000 --- a/web/src/pages/data-flow/form/components/description-field.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 ( - <FormField - control={form.control} - name={`description`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('flow.description')}</FormLabel> - <FormControl> - <Textarea {...field}></Textarea> - </FormControl> - </FormItem> - )} - /> - ); -} 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 deleted file mode 100644 index a5781fd16..000000000 --- a/web/src/pages/data-flow/form/components/dynamic-input-variable.tsx +++ /dev/null @@ -1,127 +0,0 @@ -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 ( - <Form.List name="query"> - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - <Flex key={key} gap={10} align={'baseline'}> - <Form.Item - {...restField} - name={[name, 'type']} - className={styles.variableType} - > - <Select - options={options} - onChange={handleTypeChange(name)} - ></Select> - </Form.Item> - <Form.Item noStyle dependencies={[name, 'type']}> - {({ getFieldValue }) => { - const type = getFieldValue(['query', name, 'type']); - return ( - <Form.Item - {...restField} - name={[name, getVariableName(type)]} - className={styles.variableValue} - > - {type === VariableType.Reference ? ( - <Select - placeholder={t('common.pleaseSelect')} - options={valueOptions} - ></Select> - ) : ( - <Input placeholder={t('common.pleaseInput')} /> - )} - </Form.Item> - ); - }} - </Form.Item> - <MinusCircleOutlined onClick={() => remove(name)} /> - </Flex> - ))} - <Form.Item> - <Button - type="dashed" - onClick={() => add({ type: VariableType.Reference })} - block - icon={<PlusOutlined />} - className={styles.addButton} - > - {t('flow.addVariable')} - </Button> - </Form.Item> - </> - )} - </Form.List> - ); -}; - -export function FormCollapse({ - children, - title, -}: PropsWithChildren<{ title: string }>) { - return ( - <Collapse - className={styles.dynamicInputVariable} - defaultActiveKey={['1']} - items={[ - { - key: '1', - label: <span className={styles.title}>{title}</span>, - children, - }, - ]} - /> - ); -} - -const DynamicInputVariable = ({ node }: IProps) => { - const { t } = useTranslation(); - return ( - <FormCollapse title={t('flow.input')}> - <DynamicVariableForm node={node}></DynamicVariableForm> - </FormCollapse> - ); -}; - -export default DynamicInputVariable; 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 deleted file mode 100644 index 8b4cbd8a9..000000000 --- a/web/src/pages/data-flow/form/components/next-dynamic-input-variable.tsx +++ /dev/null @@ -1,135 +0,0 @@ -'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 ( - <div> - {fields.map((field, index) => { - const typeField = `query.${index}.type`; - const typeValue = form.watch(typeField); - return ( - <div key={field.id} className="flex items-center gap-1"> - <FormField - control={form.control} - name={typeField} - render={({ field }) => ( - <FormItem className="w-2/5"> - <FormDescription /> - <FormControl> - <RAGFlowSelect - {...field} - placeholder={t('common.pleaseSelect')} - options={options} - onChange={(val) => { - field.onChange(val); - form.resetField(`query.${index}.value`); - form.resetField(`query.${index}.component_id`); - }} - ></RAGFlowSelect> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`query.${index}.${getVariableName(typeValue)}`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormDescription /> - <FormControl> - {typeValue === VariableType.Reference ? ( - <RAGFlowSelect - placeholder={t('common.pleaseSelect')} - {...field} - options={valueOptions} - ></RAGFlowSelect> - ) : ( - <Input placeholder={t('common.pleaseInput')} {...field} /> - )} - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <Trash2 - className="cursor-pointer mx-3 size-4 text-colors-text-functional-danger" - onClick={() => remove(index)} - /> - </div> - ); - })} - <Button onClick={append} className="mt-4" variant={'outline'} size={'sm'}> - <Plus /> - {t('flow.addVariable')} - </Button> - </div> - ); -} - -export function DynamicInputVariable({ node }: IProps) { - const { t } = useTranslation(); - - return ( - <Collapsible defaultOpen className="group/collapsible"> - <CollapsibleTrigger className="flex justify-between w-full pb-2"> - <span className="font-bold text-2xl text-colors-text-neutral-strong"> - {t('flow.input')} - </span> - <Button variant={'icon'} size={'icon'}> - <SideDown /> - </Button> - </CollapsibleTrigger> - <CollapsibleContent> - <DynamicVariableForm node={node}></DynamicVariableForm> - </CollapsibleContent> - </Collapsible> - ); -} 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 deleted file mode 100644 index b6cf30ed9..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/constant.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 8f3050647..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/index.css +++ /dev/null @@ -1,76 +0,0 @@ -.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 deleted file mode 100644 index caf6914b4..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/index.tsx +++ /dev/null @@ -1,181 +0,0 @@ -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<Klass<LexicalNode>> = [ - 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 ( - <section - className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })} - > - {showToolbar && ( - <div className="border-b px-2 py-2 justify-end flex"> - <Tooltip> - <TooltipTrigger asChild> - <span className="inline-block cursor-pointer cursor p-0.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-sm"> - <Variable size={16} onClick={handleVariableIconClick} /> - </span> - </TooltipTrigger> - <TooltipContent> - <p>{t('flow.insertVariableTip')}</p> - </TooltipContent> - </Tooltip> - </div> - )} - <ContentEditable - className={cn( - 'relative px-2 py-1 focus-visible:outline-none max-h-[50vh] overflow-auto', - { - 'min-h-40': multiLine, - }, - )} - onBlur={handleBlur} - onFocus={handleFocus} - /> - </section> - ); -} - -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 ( - <div className="relative"> - <LexicalComposer initialConfig={initialConfig}> - <RichTextPlugin - contentEditable={ - <PromptContent - showToolbar={showToolbar} - multiLine={multiLine} - ></PromptContent> - } - placeholder={ - <div - className={cn( - 'absolute top-1 left-2 text-text-secondary pointer-events-none', - { - 'truncate w-[90%]': !multiLine, - 'translate-y-10': multiLine, - }, - )} - > - {placeholder || t('common.promptPlaceholder')} - </div> - } - ErrorBoundary={LexicalErrorBoundary} - /> - <VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin> - <PasteHandlerPlugin /> - <VariableOnChangePlugin - onChange={onValueChange} - ></VariableOnChangePlugin> - </LexicalComposer> - </div> - ); -} 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 deleted file mode 100644 index a45a5e5fb..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/paste-handler-plugin.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 1cc2bc155..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/theme.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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 deleted file mode 100644 index 177c370c9..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/variable-node.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { BeginId } from '@/pages/flow/constant'; -import { DecoratorNode, LexicalNode, NodeKey } from 'lexical'; -import { ReactNode } from 'react'; -const prefix = BeginId + '@'; - -export class VariableNode extends DecoratorNode<ReactNode> { - __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 = ( - <div className="text-blue-600">{this.__label}</div> - ); - if (this.__parentLabel) { - content = ( - <div className="flex items-center gap-1 text-text-primary "> - <div>{this.__icon}</div> - <div>{this.__parentLabel}</div> - <div className="text-text-disabled mr-1">/</div> - {content} - </div> - ); - } - return ( - <div className="bg-gray-200 dark:bg-gray-400 text-sm inline-flex items-center rounded-md px-2 py-1"> - {content} - </div> - ); - } - - 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 deleted file mode 100644 index 86fa66db4..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/variable-on-change-plugin.tsx +++ /dev/null @@ -1,35 +0,0 @@ -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<string>, - ) => 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 deleted file mode 100644 index f429981c7..000000000 --- a/web/src/pages/data-flow/form/components/prompt-editor/variable-picker-plugin.tsx +++ /dev/null @@ -1,297 +0,0 @@ -/** - * 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 ( - <li - key={option.key} - tabIndex={-1} - ref={option.setRefElement} - role="option" - id={'typeahead-item-' + index} - > - <div> - <span className="text text-slate-500">{option.title}</span> - <ul className="pl-2 py-1"> - {option.options.map((x) => ( - <li - key={x.value} - onClick={() => selectOptionAndCleanUp(x)} - className="hover:bg-slate-300 p-1" - > - {x.label} - </li> - ))} - </ul> - </div> - </li> - ); -} - -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<string | null>(''); - - 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 ( - <LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption> - onQueryChange={setQueryString} - onSelectOption={onSelectOption} - triggerFn={checkForTriggerMatch} - options={buildNextOptions()} - menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { - const nextOptions = buildNextOptions(); - return anchorElementRef.current && nextOptions.length - ? ReactDOM.createPortal( - <div className="typeahead-popover w-[200px] p-2"> - <ul className="overflow-y-auto !scrollbar-thin overflow-x-hidden"> - {nextOptions.map((option, i: number) => ( - <VariablePickerMenuItem - index={i} - key={option.key} - option={option} - selectOptionAndCleanUp={selectOptionAndCleanUp} - /> - ))} - </ul> - </div>, - 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 deleted file mode 100644 index dafcb4cde..000000000 --- a/web/src/pages/data-flow/form/components/query-variable.tsx +++ /dev/null @@ -1,66 +0,0 @@ -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 ( - <FormField - control={form.control} - name={name} - render={({ field }) => ( - <FormItem> - {label || ( - <FormLabel tooltip={t('flow.queryTip')}> - {t('flow.query')} - </FormLabel> - )} - <FormControl> - <SelectWithSearch - options={finalOptions} - {...field} - allowClear - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} diff --git a/web/src/pages/data-flow/form/crawler-form/index.tsx b/web/src/pages/data-flow/form/crawler-form/index.tsx deleted file mode 100644 index 8c8da6b08..000000000 --- a/web/src/pages/data-flow/form/crawler-form/index.tsx +++ /dev/null @@ -1,105 +0,0 @@ -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 ( - <FormField - control={form.control} - name="proxy" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('proxy')}</FormLabel> - <FormControl> - <Input placeholder="like: http://127.0.0.1:8888" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} - -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 ( - <FormField - control={form.control} - name="extract_type" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('extractType')}</FormLabel> - <FormControl> - <SelectWithSearch {...field} options={crawlerResultOptions} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} - -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<z.infer<typeof FormSchema>>({ - resolver: zodResolver(FormSchema), - defaultValues: initialCrawlerValues, - mode: 'onChange', - }); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <form - className="space-y-6 p-4" - onSubmit={(e) => { - e.preventDefault(); - }} - > - <QueryVariable></QueryVariable> - <CrawlerProxyFormField></CrawlerProxyFormField> - <CrawlerExtractTypeFormField></CrawlerExtractTypeFormField> - </form> - </Form> - ); -} - -export default memo(CrawlerForm); diff --git a/web/src/pages/data-flow/form/email-form/index.tsx b/web/src/pages/data-flow/form/email-form/index.tsx deleted file mode 100644 index b142dae76..000000000 --- a/web/src/pages/data-flow/form/email-form/index.tsx +++ /dev/null @@ -1,161 +0,0 @@ -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 ( - <FormField - control={form.control} - name={name} - render={({ field }) => ( - <FormItem> - <FormLabel>{label}</FormLabel> - <FormControl> - <Input {...field} type={type}></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} - -function PromptFormField({ name, label }: InputFormFieldProps) { - const form = useFormContext(); - - return ( - <FormField - control={form.control} - name={name} - render={({ field }) => ( - <FormItem> - <FormLabel>{label}</FormLabel> - <FormControl> - <PromptEditor - {...field} - showToolbar={false} - multiLine={false} - ></PromptEditor> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} -export function EmailFormWidgets() { - const { t } = useTranslate('flow'); - - return ( - <> - <InputFormField - name="smtp_server" - label={t('smtpServer')} - ></InputFormField> - <InputFormField - name="smtp_port" - label={t('smtpPort')} - type="number" - ></InputFormField> - <InputFormField name="email" label={t('senderEmail')}></InputFormField> - <InputFormField - name="password" - label={t('authCode')} - type="password" - ></InputFormField> - <InputFormField - name="sender_name" - label={t('senderName')} - ></InputFormField> - </> - ); -} - -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<z.infer<typeof FormSchema>>({ - defaultValues, - resolver: zodResolver(FormSchema), - }); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <PromptFormField - name="to_email" - label={t('toEmail')} - ></PromptFormField> - <PromptFormField - name="cc_email" - label={t('ccEmail')} - ></PromptFormField> - <PromptFormField - name="content" - label={t('content')} - ></PromptFormField> - <PromptFormField - name="subject" - label={t('subject')} - ></PromptFormField> - <EmailFormWidgets></EmailFormWidgets> - </FormContainer> - </FormWrapper> - <div className="p-5"> - <Output list={outputList}></Output> - </div> - </Form> - ); -}; - -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 deleted file mode 100644 index 5a6cb5ba6..000000000 --- a/web/src/pages/data-flow/form/exesql-form/index.tsx +++ /dev/null @@ -1,167 +0,0 @@ -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 ( - <> - <FormField - control={form.control} - name="db_type" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('dbType')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={ExeSQLOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="database" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('database')}</FormLabel> - <FormControl> - <Input {...field}></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="username" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('username')}</FormLabel> - <FormControl> - <Input {...field}></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="host" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('host')}</FormLabel> - <FormControl> - <Input {...field}></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="port" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('port')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="password" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('password')}</FormLabel> - <FormControl> - <Input {...field} type="password"></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="max_records" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('maxRecords')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <div className="flex justify-end"> - <ButtonLoading loading={loading} type="submit"> - {t('test')} - </ButtonLoading> - </div> - </> - ); -} - -function ExeSQLForm({ node }: INextOperatorForm) { - const defaultValues = useFormValues(initialExeSqlValues, node); - - const { onSubmit, loading } = useSubmitForm(); - - const form = useForm<z.infer<typeof FormSchema>>({ - resolver: zodResolver(FormSchema), - defaultValues, - }); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper onSubmit={form.handleSubmit(onSubmit)}> - <QueryVariable name="sql"></QueryVariable> - <ExeSQLFormWidgets loading={loading}></ExeSQLFormWidgets> - </FormWrapper> - <div className="p-5"> - <Output list={outputList}></Output> - </div> - </Form> - ); -} - -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 deleted file mode 100644 index 8be69c7b0..000000000 --- a/web/src/pages/data-flow/form/exesql-form/use-submit-form.ts +++ /dev/null @@ -1,31 +0,0 @@ -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<typeof FormSchema>) => { - testDbConnect(data); - }, - [testDbConnect], - ); - - return { loading, onSubmit }; -} diff --git a/web/src/pages/data-flow/form/extractor-form/index.tsx b/web/src/pages/data-flow/form/extractor-form/index.tsx new file mode 100644 index 000000000..cb0abc877 --- /dev/null +++ b/web/src/pages/data-flow/form/extractor-form/index.tsx @@ -0,0 +1,102 @@ +import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { LargeModelFormField } from '@/components/large-model-form-field'; +import { LlmSettingSchema } from '@/components/llm-setting-items/next'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Form } from '@/components/ui/form'; +import { PromptEditor } from '@/pages/agent/form/components/prompt-editor'; +import { buildOptions } from '@/utils/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { + ContextGeneratorFieldName, + initialExtractorValues, +} from '../../constant'; +import { useBuildNodeOutputOptions } from '../../hooks/use-build-options'; +import { useFormValues } from '../../hooks/use-form-values'; +import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { INextOperatorForm } from '../../interface'; +import { FormWrapper } from '../components/form-wrapper'; +import { useSwitchPrompt } from './use-switch-prompt'; + +export const FormSchema = z.object({ + field_name: z.string(), + sys_prompt: z.string(), + prompts: z.string().optional(), + ...LlmSettingSchema, +}); + +export type ExtractorFormSchemaType = z.infer<typeof FormSchema>; + +const ExtractorForm = ({ node }: INextOperatorForm) => { + const defaultValues = useFormValues(initialExtractorValues, node); + const { t } = useTranslation(); + + const form = useForm<ExtractorFormSchemaType>({ + defaultValues, + resolver: zodResolver(FormSchema), + // mode: 'onChange', + }); + + const promptOptions = useBuildNodeOutputOptions(node?.id); + + const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow'); + + const { + handleFieldNameChange, + confirmSwitch, + hideModal, + visible, + cancelSwitch, + } = useSwitchPrompt(form); + + useWatchFormChange(node?.id, form); + + return ( + <Form {...form}> + <FormWrapper> + <LargeModelFormField></LargeModelFormField> + <RAGFlowFormItem label={t('dataflow.fieldName')} name="field_name"> + {(field) => ( + <SelectWithSearch + onChange={(value) => { + field.onChange(value); + handleFieldNameChange(value); + }} + value={field.value} + placeholder={t('dataFlowPlaceholder')} + options={options} + ></SelectWithSearch> + )} + </RAGFlowFormItem> + <RAGFlowFormItem label={t('flow.systemPrompt')} name="sys_prompt"> + <PromptEditor + placeholder={t('flow.messagePlaceholder')} + showToolbar={true} + baseOptions={promptOptions} + ></PromptEditor> + </RAGFlowFormItem> + <RAGFlowFormItem label={t('flow.userPrompt')} name="prompts"> + <PromptEditor + showToolbar={true} + baseOptions={promptOptions} + ></PromptEditor> + </RAGFlowFormItem> + </FormWrapper> + {visible && ( + <ConfirmDeleteDialog + title={t('dataflow.switchPromptMessage')} + open + onOpenChange={hideModal} + onOk={confirmSwitch} + onCancel={cancelSwitch} + ></ConfirmDeleteDialog> + )} + </Form> + ); +}; + +export default memo(ExtractorForm); diff --git a/web/src/pages/data-flow/form/extractor-form/use-switch-prompt.ts b/web/src/pages/data-flow/form/extractor-form/use-switch-prompt.ts new file mode 100644 index 000000000..4efb2c472 --- /dev/null +++ b/web/src/pages/data-flow/form/extractor-form/use-switch-prompt.ts @@ -0,0 +1,69 @@ +import { LlmSettingSchema } from '@/components/llm-setting-items/next'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useCallback, useRef } from 'react'; +import { UseFormReturn } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +export const FormSchema = z.object({ + field_name: z.string(), + sys_prompt: z.string(), + prompts: z.string().optional(), + ...LlmSettingSchema, +}); + +export type ExtractorFormSchemaType = z.infer<typeof FormSchema>; + +export function useSwitchPrompt(form: UseFormReturn<ExtractorFormSchemaType>) { + const { visible, showModal, hideModal } = useSetModalState(); + const { t } = useTranslation(); + const previousFieldNames = useRef<string[]>([form.getValues('field_name')]); + + const setPromptValue = useCallback( + (field: keyof ExtractorFormSchemaType, key: string, value: string) => { + form.setValue(field, t(`dataflow.prompts.${key}.${value}`), { + shouldDirty: true, + shouldValidate: true, + }); + }, + [form, t], + ); + + const handleFieldNameChange = useCallback( + (value: string) => { + if (value) { + const names = previousFieldNames.current; + if (names.length > 1) { + names.shift(); + } + names.push(value); + showModal(); + } + }, + [showModal], + ); + + const confirmSwitch = useCallback(() => { + const value = form.getValues('field_name'); + setPromptValue('sys_prompt', 'system', value); + setPromptValue('prompts', 'user', value); + }, [form, setPromptValue]); + + const cancelSwitch = useCallback(() => { + const previousValue = previousFieldNames.current.at(-2); + if (previousValue) { + form.setValue('field_name', previousValue, { + shouldDirty: true, + shouldValidate: true, + }); + } + }, [form]); + + return { + handleFieldNameChange, + confirmSwitch, + hideModal, + visible, + cancelSwitch, + }; +} diff --git a/web/src/pages/data-flow/form/hierarchical-merger-form/index.tsx b/web/src/pages/data-flow/form/hierarchical-merger-form/index.tsx new file mode 100644 index 000000000..6ce32fd6b --- /dev/null +++ b/web/src/pages/data-flow/form/hierarchical-merger-form/index.tsx @@ -0,0 +1,188 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { BlockButton, Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Form, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Plus, Trash2 } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useForm, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { Hierarchy, initialHierarchicalMergerValues } 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'; + +const outputList = buildOutputList(initialHierarchicalMergerValues.outputs); + +const HierarchyOptions = [ + { label: 'H1', value: Hierarchy.H1 }, + { label: 'H2', value: Hierarchy.H2 }, + { label: 'H3', value: Hierarchy.H3 }, + { label: 'H4', value: Hierarchy.H4 }, + { label: 'H5', value: Hierarchy.H5 }, +]; + +export const FormSchema = z.object({ + hierarchy: z.string(), + levels: z.array( + z.object({ + expressions: z.array( + z.object({ + expression: z.string().refine( + (val) => { + try { + // Try converting the string to a RegExp + new RegExp(val); + return true; + } catch { + return false; + } + }, + { + message: 'Must be a valid regular expression string', + }, + ), + }), + ), + }), + ), +}); + +export type HierarchicalMergerFormSchemaType = z.infer<typeof FormSchema>; + +type RegularExpressionsProps = { + index: number; + parentName: string; + removeParent: (index: number) => void; + isLatest: boolean; +}; + +export function RegularExpressions({ + index, + parentName, + isLatest, + removeParent, +}: RegularExpressionsProps) { + const { t } = useTranslation(); + const form = useFormContext(); + + const name = `${parentName}.${index}.expressions`; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( + <Card> + <CardHeader className="flex-row justify-between items-center"> + <span>H{index + 1}</span> + {isLatest && ( + <Button + type="button" + variant={'ghost'} + onClick={() => removeParent(index)} + > + <Trash2 /> + </Button> + )} + </CardHeader> + <CardContent> + <FormLabel required className="mb-2 text-text-secondary"> + {t('dataflow.regularExpressions')} + </FormLabel> + <section className="space-y-4"> + {fields.map((field, index) => ( + <div key={field.id} className="flex items-center gap-2"> + <div className="space-y-2 flex-1"> + <RAGFlowFormItem + name={`${name}.${index}.expression`} + label={'expression'} + labelClassName="!hidden" + > + <Input className="!m-0"></Input> + </RAGFlowFormItem> + </div> + {index === 0 ? ( + <Button + onClick={() => append({ expression: '' })} + variant={'ghost'} + > + <Plus></Plus> + </Button> + ) : ( + <Button + type="button" + variant={'ghost'} + onClick={() => remove(index)} + > + <Trash2 /> + </Button> + )} + </div> + ))} + </section> + </CardContent> + </Card> + ); +} + +const HierarchicalMergerForm = ({ node }: INextOperatorForm) => { + const { t } = useTranslation(); + const defaultValues = useFormValues(initialHierarchicalMergerValues, node); + + const form = useForm<HierarchicalMergerFormSchemaType>({ + defaultValues, + resolver: zodResolver(FormSchema), + mode: 'onChange', + }); + + const name = 'levels'; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + useWatchFormChange(node?.id, form); + + return ( + <Form {...form}> + <FormWrapper> + <RAGFlowFormItem name={'hierarchy'} label={t('dataflow.hierarchy')}> + <SelectWithSearch options={HierarchyOptions}></SelectWithSearch> + </RAGFlowFormItem> + {fields.map((field, index) => ( + <div key={field.id} className="flex items-center"> + <div className="flex-1"> + <RegularExpressions + parentName={name} + index={index} + removeParent={remove} + isLatest={index === fields.length - 1} + ></RegularExpressions> + </div> + </div> + ))} + {fields.length < 5 && ( + <BlockButton + onClick={() => append({ expressions: [{ expression: '' }] })} + > + {t('common.add')} + </BlockButton> + )} + </FormWrapper> + <div className="p-5"> + <Output list={outputList}></Output> + </div> + </Form> + ); +}; + +export default memo(HierarchicalMergerForm); diff --git a/web/src/pages/data-flow/form/invoke-form/hooks.ts b/web/src/pages/data-flow/form/invoke-form/hooks.ts deleted file mode 100644 index 951cd42ae..000000000 --- a/web/src/pages/data-flow/form/invoke-form/hooks.ts +++ /dev/null @@ -1,97 +0,0 @@ -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<HTMLInputElement> => - (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 deleted file mode 100644 index 3d67ec030..000000000 --- a/web/src/pages/data-flow/form/invoke-form/index.tsx +++ /dev/null @@ -1,226 +0,0 @@ -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 ( - <div className="flex gap-2 items-center"> - <NumberInput value={value} onChange={onChange} /> {t('flow.seconds')} - </div> - ); -}; - -const outputList = buildOutputList(initialInvokeValues.outputs); - -function InvokeForm({ node }: INextOperatorForm) { - const { t } = useTranslation(); - const defaultValues = useFormValues(initialInvokeValues, node); - - const form = useForm<FormSchemaType>({ - 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 ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <FormField - control={form.control} - name="url" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.url')}</FormLabel> - <FormControl> - <Input {...field} placeholder="http://" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="method" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.method')}</FormLabel> - <FormControl> - <SelectWithSearch {...field} options={MethodOptions} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="timeout" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.timeout')}</FormLabel> - <FormControl> - <TimeoutInput {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="headers" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.headers')}</FormLabel> - <FormControl> - <Editor - height={200} - defaultLanguage="json" - theme="vs-dark" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="proxy" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.proxy')}</FormLabel> - <FormControl> - <Input {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="clean_html" - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('flow.cleanHtmlTip')}> - {t('flow.cleanHtml')} - </FormLabel> - <FormControl> - <Switch - onCheckedChange={field.onChange} - checked={field.value} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - {/* Create a hidden field to make Form instance record this */} - <FormField - control={form.control} - name={'variables'} - render={() => <div></div>} - /> - </FormContainer> - <Collapse - title={<div>{t('flow.parameter')}</div>} - rightContent={ - <Button - variant={'ghost'} - onClick={(e) => { - e.preventDefault(); - showModal(); - }} - > - <Plus /> - </Button> - } - > - <VariableTable - data={variables} - showModal={showModal} - deleteRecord={handleDeleteRecord} - nodeId={node?.id} - ></VariableTable> - </Collapse> - {visible && ( - <VariableDialog - hideModal={hideModal} - initialValue={currentRecord} - otherThanCurrentQuery={otherThanCurrentQuery} - submit={ok} - ></VariableDialog> - )} - </FormWrapper> - <div className="p-5"> - <Output list={outputList}></Output> - </div> - </Form> - ); -} - -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 deleted file mode 100644 index a3b11aff2..000000000 --- a/web/src/pages/data-flow/form/invoke-form/schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -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<typeof FormSchema>; - -export type VariableFormSchemaType = z.infer<typeof VariableFormSchema>; 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 deleted file mode 100644 index 40b371894..000000000 --- a/web/src/pages/data-flow/form/invoke-form/use-edit-variable.ts +++ /dev/null @@ -1,70 +0,0 @@ -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<FormSchemaType> }) => { - const { setRecord, currentRecord } = - useSetSelectedRecord<VariableFormSchemaType>(); - - 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 deleted file mode 100644 index 03c4d83b0..000000000 --- a/web/src/pages/data-flow/form/invoke-form/variable-dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -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<z.infer<typeof FormSchema>>({ - resolver: zodResolver(FormSchema), - mode: 'onChange', - defaultValues: { - key: '', - value: '', - ref: '', - }, - }); - - useEffect(() => { - if (!isEmpty(initialValue)) { - form.reset(initialValue); - } - }, [form, initialValue]); - - function onSubmit(data: z.infer<typeof FormSchema>) { - submit(data); - } - - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - id={FormId} - className="space-y-5" - autoComplete="off" - > - <FormField - name="key" - control={form.control} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.key')}</FormLabel> - <FormControl> - <Input {...field} autoComplete="off" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <QueryVariable name="ref" label={t('flow.ref')}></QueryVariable> - <FormField - name="value" - control={form.control} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.value')}</FormLabel> - <FormControl> - <Input {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </form> - </Form> - ); -} - -export function VariableDialog({ - initialValue, - hideModal, - otherThanCurrentQuery, - submit, -}: ModalFormProps & IModalProps<VariableFormSchemaType>) { - const { t } = useTranslation(); - - return ( - <Dialog open onOpenChange={hideModal}> - <DialogContent> - <DialogHeader> - <DialogTitle>{t('flow.variableSettings')}</DialogTitle> - </DialogHeader> - <VariableForm - initialValue={initialValue} - otherThanCurrentQuery={otherThanCurrentQuery} - submit={submit} - ></VariableForm> - <DialogFooter> - <Button type="submit" form={FormId}> - {t('modal.okText')} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} 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 deleted file mode 100644 index 33a747670..000000000 --- a/web/src/pages/data-flow/form/invoke-form/variable-table.tsx +++ /dev/null @@ -1,199 +0,0 @@ -'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<SortingState>([]); - const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState<VisibilityState>({}); - - const columns: ColumnDef<VariableFormSchemaType>[] = [ - { - accessorKey: 'key', - header: t('flow.key'), - meta: { cellClassName: 'max-w-30' }, - cell: ({ row }) => { - const key: string = row.getValue('key'); - return ( - <Tooltip> - <TooltipTrigger asChild> - <div className="truncate">{key}</div> - </TooltipTrigger> - <TooltipContent> - <p>{key}</p> - </TooltipContent> - </Tooltip> - ); - }, - }, - { - accessorKey: 'ref', - header: t('flow.ref'), - meta: { cellClassName: 'max-w-30' }, - cell: ({ row }) => { - const ref: string = row.getValue('ref'); - const label = getLabel(ref); - return ( - <Tooltip> - <TooltipTrigger asChild> - <div className="truncate">{label}</div> - </TooltipTrigger> - <TooltipContent> - <p>{label}</p> - </TooltipContent> - </Tooltip> - ); - }, - }, - { - accessorKey: 'value', - header: t('flow.value'), - cell: ({ row }) => <div>{row.getValue('value')}</div>, - }, - { - id: 'actions', - enableHiding: false, - header: t('common.action'), - cell: ({ row }) => { - const record = row.original; - const idx = row.index; - - return ( - <div> - <Button - className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground" - onClick={() => showModal(idx, record)} - > - <Pencil /> - </Button> - <Button - className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground" - onClick={() => deleteRecord(idx)} - > - <Trash2 /> - </Button> - </div> - ); - }, - }, - ]; - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - state: { - sorting, - columnFilters, - columnVisibility, - }, - }); - - return ( - <div className="w-full"> - <div className="rounded-md border"> - <Table rootClassName="rounded-md"> - <TableHeader> - {table.getHeaderGroups().map((headerGroup) => ( - <TableRow key={headerGroup.id}> - {headerGroup.headers.map((header) => { - return ( - <TableHead key={header.id}> - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - </TableHead> - ); - })} - </TableRow> - ))} - </TableHeader> - <TableBody> - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - <TableRow - key={row.id} - data-state={row.getIsSelected() && 'selected'} - > - {row.getVisibleCells().map((cell) => ( - <TableCell - key={cell.id} - className={cn(cell.column.columnDef.meta?.cellClassName)} - > - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - </TableCell> - ))} - </TableRow> - )) - ) : ( - <TableEmpty columnsLength={columns.length}></TableEmpty> - )} - </TableBody> - </Table> - </div> - </div> - ); -} 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 deleted file mode 100644 index c31be8fd0..000000000 --- a/web/src/pages/data-flow/form/iteration-form/dynamic-output.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'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 ( - <div className="space-y-5"> - {fields.map((field, index) => { - const nameField = `${name}.${index}.name`; - const typeField = `${name}.${index}.type`; - return ( - <div key={field.id} className="flex items-center gap-2"> - <FormField - control={form.control} - name={nameField} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormControl> - <Input - {...field} - placeholder={t('common.pleaseInput')} - ></Input> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <Separator className="w-3 text-text-secondary" /> - <FormField - control={form.control} - name={`${name}.${index}.ref`} - render={({ field }) => ( - <FormItem className="w-2/5"> - <FormControl> - <SelectWithSearch - options={options} - {...field} - onChange={(val) => { - form.setValue(typeField, findType(val)); - field.onChange(val); - }} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={typeField} - render={() => <div></div>} - /> - <Button variant={'ghost'} onClick={() => remove(index)}> - <X className="text-text-sub-title-invert " /> - </Button> - </div> - ); - })} - <BlockButton onClick={() => append({ name: '', ref: undefined })}> - {t('common.add')} - </BlockButton> - </div> - ); -} - -export function VariableTitle({ title }: { title: ReactNode }) { - return <div className="font-medium text-text-primary pb-2">{title}</div>; -} - -export function DynamicOutput({ node }: IProps) { - return ( - <FormContainer> - <VariableTitle title={t('flow.output')}></VariableTitle> - <DynamicOutputForm node={node}></DynamicOutputForm> - </FormContainer> - ); -} diff --git a/web/src/pages/data-flow/form/iteration-form/index.tsx b/web/src/pages/data-flow/form/iteration-form/index.tsx deleted file mode 100644 index c70b764fb..000000000 --- a/web/src/pages/data-flow/form/iteration-form/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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 ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <QueryVariable - name="items_ref" - type={VariableType.Array} - ></QueryVariable> - </FormContainer> - <DynamicOutput node={node}></DynamicOutput> - <Output list={outputList}></Output> - </FormWrapper> - </Form> - ); -} - -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 deleted file mode 100644 index 25f22aab0..000000000 --- a/web/src/pages/data-flow/form/iteration-form/interface.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type OutputArray = Array<{ name: string; ref: string; type?: string }>; -export type OutputObject = Record<string, { ref: string; type?: string }>; 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 deleted file mode 100644 index 3439000d4..000000000 --- a/web/src/pages/data-flow/form/iteration-form/use-build-options.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 29cd06324..000000000 --- a/web/src/pages/data-flow/form/iteration-form/use-values.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 4a780667e..000000000 --- a/web/src/pages/data-flow/form/iteration-form/use-watch-form-change.ts +++ /dev/null @@ -1,30 +0,0 @@ -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<OutputObject>((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 deleted file mode 100644 index ba58b92d1..000000000 --- a/web/src/pages/data-flow/form/iteration-start-from/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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<OutputType[]>( - (pre, [key, value]) => { - pre.push({ title: key, type: value.type }); - - return pre; - }, - [], -); -function IterationStartForm() { - return ( - <section className="space-y-6 p-4"> - <Output list={outputList}></Output> - </section> - ); -} - -export default memo(IterationStartForm); 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 deleted file mode 100644 index bda5d44f5..000000000 --- a/web/src/pages/data-flow/form/keyword-extract-form/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -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 ( - <Form {...form}> - <form - className="space-y-6" - onSubmit={(e) => { - e.preventDefault(); - }} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <FormField - control={form.control} - name="llm_id" - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('chat.modelTip')}> - {t('chat.model')} - </FormLabel> - <FormControl> - <NextLLMSelect {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <TopNFormField></TopNFormField> - </form> - </Form> - ); -}; - -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 deleted file mode 100644 index 05c831a84..000000000 --- a/web/src/pages/data-flow/form/message-form/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -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 ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <FormItem> - <FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel> - <div className="space-y-4"> - {fields.map((field, index) => ( - <div key={field.id} className="flex items-start gap-2"> - <FormField - control={form.control} - name={`content.${index}.value`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormControl> - {/* <Textarea {...field}> </Textarea> */} - <PromptEditor - {...field} - placeholder={t('flow.messagePlaceholder')} - ></PromptEditor> - </FormControl> - </FormItem> - )} - /> - {fields.length > 1 && ( - <Button - type="button" - variant={'ghost'} - onClick={() => remove(index)} - > - <X /> - </Button> - )} - </div> - ))} - - <BlockButton - type="button" - onClick={() => append({ value: '' })} // "" will cause the inability to add, refer to: https://github.com/orgs/react-hook-form/discussions/8485#discussioncomment-2961861 - > - {t('flow.addMessage')} - </BlockButton> - </div> - <FormMessage /> - </FormItem> - </FormContainer> - </FormWrapper> - </Form> - ); -} - -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 deleted file mode 100644 index 6a90881be..000000000 --- a/web/src/pages/data-flow/form/message-form/use-values.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 10c35c653..000000000 --- a/web/src/pages/data-flow/form/message-form/use-watch-change.ts +++ /dev/null @@ -1,24 +0,0 @@ -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/parser-form/common-form-fields.tsx b/web/src/pages/data-flow/form/parser-form/common-form-fields.tsx new file mode 100644 index 000000000..46809ad2c --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/common-form-fields.tsx @@ -0,0 +1,85 @@ +import { crossLanguageOptions } from '@/components/cross-language-form-field'; +import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field'; +import { LLMFormField } from '@/components/llm-setting-items/llm-form-field'; +import { + SelectWithSearch, + SelectWithSearchFlagOptionType, +} from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { buildOptions } from '@/utils/form'; +import { useTranslation } from 'react-i18next'; +import { FileType, OutputFormatMap } from '../../constant'; +import { CommonProps } from './interface'; +import { buildFieldNameWithPrefix } from './utils'; + +function buildOutputOptionsFormatMap() { + return Object.entries(OutputFormatMap).reduce< + Record<string, SelectWithSearchFlagOptionType[]> + >((pre, [key, value]) => { + pre[key] = buildOptions(value); + return pre; + }, {}); +} + +export type OutputFormatFormFieldProps = CommonProps & { + fileType: FileType; +}; + +export function OutputFormatFormField({ + prefix, + fileType, +}: OutputFormatFormFieldProps) { + const { t } = useTranslation(); + return ( + <RAGFlowFormItem + name={buildFieldNameWithPrefix(`output_format`, prefix)} + label={t('dataflow.outputFormat')} + > + <SelectWithSearch + options={buildOutputOptionsFormatMap()[fileType]} + ></SelectWithSearch> + </RAGFlowFormItem> + ); +} + +export function ParserMethodFormField({ + prefix, + optionsWithoutLLM, +}: CommonProps & { optionsWithoutLLM?: { value: string; label: string }[] }) { + const { t } = useTranslation(); + return ( + <LayoutRecognizeFormField + name={buildFieldNameWithPrefix(`parse_method`, prefix)} + horizontal={false} + optionsWithoutLLM={optionsWithoutLLM} + label={t('dataflow.parserMethod')} + ></LayoutRecognizeFormField> + ); +} + +export function LargeModelFormField({ prefix }: CommonProps) { + return ( + <LLMFormField + name={buildFieldNameWithPrefix('llm_id', prefix)} + ></LLMFormField> + ); +} + +export function LanguageFormField({ prefix }: CommonProps) { + const { t } = useTranslation(); + + return ( + <RAGFlowFormItem + name={buildFieldNameWithPrefix(`lang`, prefix)} + label={t('dataflow.lang')} + > + {(field) => ( + <SelectWithSearch + options={crossLanguageOptions} + value={field.value} + onChange={field.onChange} + ></SelectWithSearch> + )} + </RAGFlowFormItem> + ); +} diff --git a/web/src/pages/data-flow/form/parser-form/email-form-fields.tsx b/web/src/pages/data-flow/form/parser-form/email-form-fields.tsx new file mode 100644 index 000000000..640c271f9 --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/email-form-fields.tsx @@ -0,0 +1,30 @@ +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { MultiSelect } from '@/components/ui/multi-select'; +import { buildOptions } from '@/utils/form'; +import { useTranslation } from 'react-i18next'; +import { ParserFields } from '../../constant'; +import { CommonProps } from './interface'; +import { buildFieldNameWithPrefix } from './utils'; + +const options = buildOptions(ParserFields); + +export function EmailFormFields({ prefix }: CommonProps) { + const { t } = useTranslation(); + return ( + <> + <RAGFlowFormItem + name={buildFieldNameWithPrefix(`fields`, prefix)} + label={t('dataflow.fields')} + > + {(field) => ( + <MultiSelect + options={options} + onValueChange={field.onChange} + defaultValue={field.value} + variant="inverted" + ></MultiSelect> + )} + </RAGFlowFormItem> + </> + ); +} diff --git a/web/src/pages/data-flow/form/parser-form/image-form-fields.tsx b/web/src/pages/data-flow/form/parser-form/image-form-fields.tsx new file mode 100644 index 000000000..4cff99ea7 --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/image-form-fields.tsx @@ -0,0 +1,57 @@ +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Textarea } from '@/components/ui/textarea'; +import { buildOptions } from '@/utils/form'; +import { isEmpty } from 'lodash'; +import { useEffect, useMemo } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { ImageParseMethod } from '../../constant'; +import { LanguageFormField, ParserMethodFormField } from './common-form-fields'; +import { CommonProps } from './interface'; +import { useSetInitialLanguage } from './use-set-initial-language'; +import { buildFieldNameWithPrefix } from './utils'; + +const options = buildOptions(ImageParseMethod); + +export function ImageFormFields({ prefix }: CommonProps) { + const { t } = useTranslation(); + const form = useFormContext(); + const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix); + + const parseMethod = useWatch({ + name: parseMethodName, + }); + + const languageShown = useMemo(() => { + return !isEmpty(parseMethod) && parseMethod !== ImageParseMethod.OCR; + }, [parseMethod]); + + useEffect(() => { + if (isEmpty(form.getValues(parseMethodName))) { + form.setValue(parseMethodName, ImageParseMethod.OCR, { + shouldValidate: true, + shouldDirty: true, + }); + } + }, [form, parseMethodName]); + + useSetInitialLanguage({ prefix, languageShown }); + + return ( + <> + <ParserMethodFormField + prefix={prefix} + optionsWithoutLLM={options} + ></ParserMethodFormField> + {languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>} + {languageShown && ( + <RAGFlowFormItem + name={buildFieldNameWithPrefix('system_prompt', prefix)} + label={t('dataflow.systemPrompt')} + > + <Textarea placeholder={t('dataflow.systemPromptPlaceholder')} /> + </RAGFlowFormItem> + )} + </> + ); +} diff --git a/web/src/pages/data-flow/form/parser-form/index.tsx b/web/src/pages/data-flow/form/parser-form/index.tsx index 1a84c304a..2d5f540e2 100644 --- a/web/src/pages/data-flow/form/parser-form/index.tsx +++ b/web/src/pages/data-flow/form/parser-form/index.tsx @@ -1,135 +1,202 @@ -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 { RAGFlowFormItem } from '@/components/ragflow-form'; +import { BlockButton, Button } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { Separator } from '@/components/ui/separator'; +import { cn } from '@/lib/utils'; +import { buildOptions } from '@/utils/form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { memo } from 'react'; -import { useForm, useFormContext } from 'react-hook-form'; +import { useHover } from 'ahooks'; +import { Trash2 } from 'lucide-react'; +import { memo, useCallback, useMemo, useRef } from 'react'; +import { + UseFieldArrayRemove, + useFieldArray, + useForm, + useFormContext, +} from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { initialParserValues } from '../../constant'; +import { + FileType, + InitialOutputFormatMap, + initialParserValues, +} 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'; +import { OutputFormatFormField } from './common-form-fields'; +import { EmailFormFields } from './email-form-fields'; +import { ImageFormFields } from './image-form-fields'; +import { PdfFormFields } from './pdf-form-fields'; +import { buildFieldNameWithPrefix } from './utils'; +import { VideoFormFields } from './video-form-fields'; const outputList = buildOutputList(initialParserValues.outputs); -export const GoogleFormPartialSchema = { - api_key: z.string(), - country: z.string(), - language: z.string(), +const FileFormatOptions = buildOptions(FileType).filter( + (x) => x.value !== FileType.Video, // Temporarily hide the video option +); + +const FileFormatWidgetMap = { + [FileType.PDF]: PdfFormFields, + [FileType.Video]: VideoFormFields, + [FileType.Audio]: VideoFormFields, + [FileType.Email]: EmailFormFields, + [FileType.Image]: ImageFormFields, +}; + +type ParserItemProps = { + name: string; + index: number; + fieldLength: number; + remove: UseFieldArrayRemove; }; export const FormSchema = z.object({ - ...GoogleFormPartialSchema, - q: z.string(), - start: z.number(), - num: z.number(), + setups: z.array( + z.object({ + fileFormat: z.string().nullish(), + output_format: z.string().optional(), + parse_method: z.string().optional(), + lang: z.string().optional(), + fields: z.array(z.string()).optional(), + llm_id: z.string().optional(), + system_prompt: z.string().optional(), + }), + ), }); -export function GoogleFormWidgets() { - const form = useFormContext(); - const { t } = useTranslate('flow'); +export type ParserFormSchemaType = z.infer<typeof FormSchema>; + +function ParserItem({ name, index, fieldLength, remove }: ParserItemProps) { + const { t } = useTranslation(); + const form = useFormContext<ParserFormSchemaType>(); + const ref = useRef(null); + const isHovering = useHover(ref); + + const prefix = `${name}.${index}`; + const fileFormat = form.getValues(`setups.${index}.fileFormat`); + + const values = form.getValues(); + const parserList = values.setups.slice(); // Adding, deleting, or modifying the parser array will not change the reference. + + const filteredFileFormatOptions = useMemo(() => { + const otherFileFormatList = parserList + .filter((_, idx) => idx !== index) + .map((x) => x.fileFormat); + + return FileFormatOptions.filter((x) => { + return !otherFileFormatList.includes(x.value); + }); + }, [index, parserList]); + + const Widget = + typeof fileFormat === 'string' && fileFormat in FileFormatWidgetMap + ? FileFormatWidgetMap[fileFormat as keyof typeof FileFormatWidgetMap] + : () => <></>; + + const handleFileTypeChange = useCallback( + (value: FileType) => { + form.setValue( + `setups.${index}.output_format`, + InitialOutputFormatMap[value], + { shouldDirty: true, shouldValidate: true, shouldTouch: true }, + ); + }, + [form, index], + ); return ( - <> - <FormField - control={form.control} - name={`country`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('country')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={GoogleCountryOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> + <section + className={cn('space-y-5 py-2.5 rounded-md', { + 'bg-state-error-5': isHovering, + })} + > + <div className="flex justify-between items-center"> + <span className="text-text-primary text-sm font-medium"> + Parser {index + 1} + </span> + {index > 0 && ( + <Button variant={'ghost'} onClick={() => remove(index)} ref={ref}> + <Trash2 /> + </Button> )} - /> - <FormField - control={form.control} - name={`language`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('language')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={GoogleLanguageOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> + </div> + <RAGFlowFormItem + name={buildFieldNameWithPrefix(`fileFormat`, prefix)} + label={t('dataflow.fileFormats')} + > + {(field) => ( + <SelectWithSearch + value={field.value} + onChange={(val) => { + field.onChange(val); + handleFileTypeChange(val as FileType); + }} + options={filteredFileFormatOptions} + ></SelectWithSearch> )} + </RAGFlowFormItem> + <Widget prefix={prefix} fileType={fileFormat as FileType}></Widget> + <OutputFormatFormField + prefix={prefix} + fileType={fileFormat as FileType} /> - </> + {index < fieldLength - 1 && <Separator />} + </section> ); } const ParserForm = ({ node }: INextOperatorForm) => { - const { t } = useTranslate('flow'); + const { t } = useTranslation(); const defaultValues = useFormValues(initialParserValues, node); const form = useForm<z.infer<typeof FormSchema>>({ defaultValues, resolver: zodResolver(FormSchema), + shouldUnregister: true, }); + const name = 'setups'; + const { fields, remove, append } = useFieldArray({ + name, + control: form.control, + }); + + const add = useCallback(() => { + append({ + fileFormat: null, + output_format: '', + parse_method: '', + lang: '', + fields: [], + llm_id: '', + }); + }, [append]); + useWatchFormChange(node?.id, form); return ( <Form {...form}> - <FormWrapper> - <FormContainer> - <QueryVariable name="q"></QueryVariable> - </FormContainer> - <FormContainer> - <ApiKeyField placeholder={t('apiKeyPlaceholder')}></ApiKeyField> - <FormField - control={form.control} - name={`start`} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flowStart')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`num`} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flowNum')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <GoogleFormWidgets></GoogleFormWidgets> - </FormContainer> - </FormWrapper> + <form className="px-5"> + {fields.map((field, index) => { + return ( + <ParserItem + key={field.id} + name={name} + index={index} + fieldLength={fields.length} + remove={remove} + ></ParserItem> + ); + })} + <BlockButton onClick={add} type="button" className="mt-2.5"> + {t('dataflow.addParser')} + </BlockButton> + </form> <div className="p-5"> <Output list={outputList}></Output> </div> diff --git a/web/src/pages/data-flow/form/parser-form/interface.ts b/web/src/pages/data-flow/form/parser-form/interface.ts new file mode 100644 index 000000000..698ba8e64 --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/interface.ts @@ -0,0 +1,3 @@ +export type CommonProps = { + prefix: string; +}; diff --git a/web/src/pages/data-flow/form/parser-form/pdf-form-fields.tsx b/web/src/pages/data-flow/form/parser-form/pdf-form-fields.tsx new file mode 100644 index 000000000..d6c3eb7f4 --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/pdf-form-fields.tsx @@ -0,0 +1,44 @@ +import { ParseDocumentType } from '@/components/layout-recognize-form-field'; +import { isEmpty } from 'lodash'; +import { useEffect, useMemo } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { LanguageFormField, ParserMethodFormField } from './common-form-fields'; +import { CommonProps } from './interface'; +import { useSetInitialLanguage } from './use-set-initial-language'; +import { buildFieldNameWithPrefix } from './utils'; + +export function PdfFormFields({ prefix }: CommonProps) { + const form = useFormContext(); + + const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix); + + const parseMethod = useWatch({ + name: parseMethodName, + }); + + const languageShown = useMemo(() => { + return ( + !isEmpty(parseMethod) && + parseMethod !== ParseDocumentType.DeepDOC && + parseMethod !== ParseDocumentType.PlainText + ); + }, [parseMethod]); + + useSetInitialLanguage({ prefix, languageShown }); + + useEffect(() => { + if (isEmpty(form.getValues(parseMethodName))) { + form.setValue(parseMethodName, ParseDocumentType.DeepDOC, { + shouldValidate: true, + shouldDirty: true, + }); + } + }, [form, parseMethodName]); + + return ( + <> + <ParserMethodFormField prefix={prefix}></ParserMethodFormField> + {languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>} + </> + ); +} diff --git a/web/src/pages/data-flow/form/parser-form/use-set-initial-language.ts b/web/src/pages/data-flow/form/parser-form/use-set-initial-language.ts new file mode 100644 index 000000000..0eb56cfa9 --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/use-set-initial-language.ts @@ -0,0 +1,29 @@ +import { crossLanguageOptions } from '@/components/cross-language-form-field'; +import { isEmpty } from 'lodash'; +import { useEffect } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { buildFieldNameWithPrefix } from './utils'; + +export function useSetInitialLanguage({ + prefix, + languageShown, +}: { + prefix: string; + languageShown: boolean; +}) { + const form = useFormContext(); + const lang = form.getValues(buildFieldNameWithPrefix('lang', prefix)); + + useEffect(() => { + if (languageShown && isEmpty(lang)) { + form.setValue( + buildFieldNameWithPrefix('lang', prefix), + crossLanguageOptions[0].value, + { + shouldValidate: true, + shouldDirty: true, + }, + ); + } + }, [form, lang, languageShown, prefix]); +} diff --git a/web/src/pages/data-flow/form/parser-form/utils.ts b/web/src/pages/data-flow/form/parser-form/utils.ts new file mode 100644 index 000000000..851429f77 --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/utils.ts @@ -0,0 +1,3 @@ +export function buildFieldNameWithPrefix(name: string, prefix: string) { + return `${prefix}.${name}`; +} diff --git a/web/src/pages/data-flow/form/parser-form/video-form-fields.tsx b/web/src/pages/data-flow/form/parser-form/video-form-fields.tsx new file mode 100644 index 000000000..79f64d6fd --- /dev/null +++ b/web/src/pages/data-flow/form/parser-form/video-form-fields.tsx @@ -0,0 +1,13 @@ +import { + LargeModelFormField, + OutputFormatFormFieldProps, +} from './common-form-fields'; + +export function VideoFormFields({ prefix }: OutputFormatFormFieldProps) { + return ( + <> + {/* Multimodal Model */} + <LargeModelFormField prefix={prefix}></LargeModelFormField> + </> + ); +} diff --git a/web/src/pages/data-flow/form/relevant-form/hooks.ts b/web/src/pages/data-flow/form/relevant-form/hooks.ts deleted file mode 100644 index 413a0ac38..000000000 --- a/web/src/pages/data-flow/form/relevant-form/hooks.ts +++ /dev/null @@ -1,41 +0,0 @@ -import pick from 'lodash/pick'; -import { useCallback, useEffect } from 'react'; -import { IOperatorForm } from '../../interface'; -import useGraphStore from '../../store'; - -export const useBuildRelevantOptions = () => { - const nodes = useGraphStore((state) => state.nodes); - - const buildRelevantOptions = useCallback( - (toList: string[]) => { - return nodes - .filter( - (x) => !toList.some((y) => y === x.id), // filter out selected values ​​in other to fields from the current drop-down box options - ) - .map((x) => ({ label: x.data.name, value: x.id })); - }, - [nodes], - ); - - return buildRelevantOptions; -}; - -/** - * monitor changes in the connection and synchronize the target to the yes and no fields of the form - * similar to the categorize-form's useHandleFormValuesChange method - * @param param0 - */ -export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => { - const getNode = useGraphStore((state) => state.getNode); - const node = getNode(nodeId); - - const watchFormChanges = useCallback(() => { - if (node) { - form?.setFieldsValue(pick(node, ['yes', 'no'])); - } - }, [node, form]); - - useEffect(() => { - watchFormChanges(); - }, [watchFormChanges]); -}; diff --git a/web/src/pages/data-flow/form/relevant-form/index.tsx b/web/src/pages/data-flow/form/relevant-form/index.tsx deleted file mode 100644 index e2366f6f0..000000000 --- a/web/src/pages/data-flow/form/relevant-form/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { Operator } from '../../constant'; -import { useBuildFormSelectOptions } from '../../form-hooks'; -import { IOperatorForm } from '../../interface'; -import { useWatchConnectionChanges } from './hooks'; - -const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const buildRelevantOptions = useBuildFormSelectOptions( - Operator.Relevant, - node?.id, - ); - useWatchConnectionChanges({ nodeId: node?.id, form }); - - return ( - <Form - name="basic" - labelCol={{ span: 4 }} - wrapperCol={{ span: 20 }} - onValuesChange={onValuesChange} - autoComplete="off" - form={form} - > - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect></LLMSelect> - </Form.Item> - <Form.Item label={t('yes')} name={'yes'}> - <Select - allowClear - options={buildRelevantOptions([form?.getFieldValue('no')])} - /> - </Form.Item> - <Form.Item label={t('no')} name={'no'}> - <Select - allowClear - options={buildRelevantOptions([form?.getFieldValue('yes')])} - /> - </Form.Item> - </Form> - ); -}; - -export default RelevantForm; diff --git a/web/src/pages/data-flow/form/retrieval-form/next.tsx b/web/src/pages/data-flow/form/retrieval-form/next.tsx index 69204d4da..e69de29bb 100644 --- a/web/src/pages/data-flow/form/retrieval-form/next.tsx +++ b/web/src/pages/data-flow/form/retrieval-form/next.tsx @@ -1,130 +0,0 @@ -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 ( - <FormField - control={form.control} - name="empty_response" - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('chat.emptyResponseTip')}> - {t('chat.emptyResponse')} - </FormLabel> - <FormControl> - <Textarea - placeholder={t('common.namePlaceholder')} - {...field} - autoComplete="off" - rows={4} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - ); -} - -function RetrievalForm({ node }: INextOperatorForm) { - const { t } = useTranslation(); - - const outputList = useMemo(() => { - return [ - { - title: 'formalized_content', - type: initialRetrievalValues.outputs.formalized_content.type, - }, - { - title: 'json', - type: initialRetrievalValues.outputs.json.type, - }, - ]; - }, []); - - const defaultValues = useValues(node); - - const form = useForm({ - defaultValues: defaultValues, - resolver: zodResolver(FormSchema), - }); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <RAGFlowFormItem name="query" label={t('flow.query')}> - <PromptEditor></PromptEditor> - </RAGFlowFormItem> - <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> - </FormContainer> - <Collapse title={<div>{t('flow.advancedSettings')}</div>}> - <FormContainer> - <SimilaritySliderFormField - vectorSimilarityWeightName="keywords_similarity_weight" - isTooltipShown - ></SimilaritySliderFormField> - <TopNFormField></TopNFormField> - <RerankFormFields></RerankFormFields> - <EmptyResponseField></EmptyResponseField> - <CrossLanguageFormField name="cross_languages"></CrossLanguageFormField> - <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> - </FormContainer> - </Collapse> - <Output list={outputList}></Output> - </FormWrapper> - </Form> - ); -} - -export default memo(RetrievalForm); diff --git a/web/src/pages/data-flow/form/retrieval-form/use-values.ts b/web/src/pages/data-flow/form/retrieval-form/use-values.ts deleted file mode 100644 index 1718645f0..000000000 --- a/web/src/pages/data-flow/form/retrieval-form/use-values.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { isEmpty } from 'lodash'; -import { useMemo } from 'react'; -import { initialRetrievalValues } from '../../constant'; - -export function useValues(node?: RAGFlowNodeType) { - const defaultValues = useMemo( - () => ({ - ...initialRetrievalValues, - }), - [], - ); - - 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/form/rewrite-question-form/index.tsx b/web/src/pages/data-flow/form/rewrite-question-form/index.tsx deleted file mode 100644 index 4c9466176..000000000 --- a/web/src/pages/data-flow/form/rewrite-question-form/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { NextLLMSelect } from '@/components/llm-select/next'; -import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { RAGFlowSelect } from '@/components/ui/select'; -import { useTranslation } from 'react-i18next'; -import { INextOperatorForm } from '../../interface'; -import { GoogleLanguageOptions } from '../../options'; - -const RewriteQuestionForm = ({ form }: INextOperatorForm) => { - const { t } = useTranslation(); - - return ( - <Form {...form}> - <form - className="space-y-6" - onSubmit={(e) => { - e.preventDefault(); - }} - > - <FormField - control={form.control} - name="llm_id" - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('chat.modelTip')}> - {t('chat.model')} - </FormLabel> - <FormControl> - <NextLLMSelect {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="language" - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('chat.languageTip')}> - {t('chat.language')} - </FormLabel> - <FormControl> - <RAGFlowSelect - options={GoogleLanguageOptions} - allowClear={true} - {...field} - ></RAGFlowSelect> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> - </form> - </Form> - ); -}; - -export default RewriteQuestionForm; diff --git a/web/src/pages/data-flow/form/splitter-form/index.tsx b/web/src/pages/data-flow/form/splitter-form/index.tsx new file mode 100644 index 000000000..60676b5f6 --- /dev/null +++ b/web/src/pages/data-flow/form/splitter-form/index.tsx @@ -0,0 +1,102 @@ +import { DelimiterInput } from '@/components/delimiter-form-field'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { SliderInputFormField } from '@/components/slider-input-form-field'; +import { BlockButton, Button } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Trash2 } from 'lucide-react'; +import { memo } from 'react'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { initialSplitterValues } 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'; + +const outputList = buildOutputList(initialSplitterValues.outputs); + +export const FormSchema = z.object({ + chunk_token_size: z.number(), + delimiters: z.array( + z.object({ + value: z.string().optional(), + }), + ), + overlapped_percent: z.number(), // 0.0 - 0.3 +}); + +export type SplitterFormSchemaType = z.infer<typeof FormSchema>; + +const SplitterForm = ({ node }: INextOperatorForm) => { + const defaultValues = useFormValues(initialSplitterValues, node); + const { t } = useTranslation(); + + const form = useForm<SplitterFormSchemaType>({ + defaultValues, + resolver: zodResolver(FormSchema), + }); + const name = 'delimiters'; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + useWatchFormChange(node?.id, form); + + return ( + <Form {...form}> + <FormWrapper> + <SliderInputFormField + name="chunk_token_size" + max={2048} + label={t('knowledgeConfiguration.chunkTokenNumber')} + ></SliderInputFormField> + <SliderInputFormField + name="overlapped_percent" + max={0.3} + min={0} + step={0.01} + label={t('dataflow.overlappedPercent')} + ></SliderInputFormField> + <section> + <span className="mb-2 inline-block">{t('flow.delimiters')}</span> + <div className="space-y-4"> + {fields.map((field, index) => ( + <div key={field.id} className="flex items-center gap-2"> + <div className="space-y-2 flex-1"> + <RAGFlowFormItem + name={`${name}.${index}.value`} + label="delimiter" + labelClassName="!hidden" + > + <DelimiterInput className="!m-0"></DelimiterInput> + </RAGFlowFormItem> + </div> + <Button + type="button" + variant={'ghost'} + onClick={() => remove(index)} + > + <Trash2 /> + </Button> + </div> + ))} + </div> + </section> + <BlockButton onClick={() => append({ value: '\n' })}> + {t('common.add')} + </BlockButton> + </FormWrapper> + <div className="p-5"> + <Output list={outputList}></Output> + </div> + </Form> + ); +}; + +export default memo(SplitterForm); diff --git a/web/src/pages/data-flow/form/string-transform-form/index.tsx b/web/src/pages/data-flow/form/string-transform-form/index.tsx deleted file mode 100644 index dc9258a18..000000000 --- a/web/src/pages/data-flow/form/string-transform-form/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { FormContainer } from '@/components/form-container'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { MultiSelect } from '@/components/ui/multi-select'; -import { RAGFlowSelect } from '@/components/ui/select'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { t } from 'i18next'; -import { toLower } from 'lodash'; -import { memo, useCallback, useMemo } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; -import { z } from 'zod'; -import { - StringTransformDelimiter, - StringTransformMethod, - initialStringTransformValues, -} from '../../constant'; -import { INextOperatorForm } from '../../interface'; -import { FormWrapper } from '../components/form-wrapper'; -import { Output, transferOutputs } from '../components/output'; -import { PromptEditor } from '../components/prompt-editor'; -import { QueryVariable } from '../components/query-variable'; -import { useValues } from './use-values'; -import { useWatchFormChange } from './use-watch-form-change'; - -const DelimiterOptions = Object.entries(StringTransformDelimiter).map( - ([key, val]) => ({ label: t('flow.' + toLower(key)), value: val }), -); - -function StringTransformForm({ node }: INextOperatorForm) { - const values = useValues(node); - - const FormSchema = z.object({ - method: z.string(), - split_ref: z.string().optional(), - script: z.string().optional(), - delimiters: z.array(z.string()).or(z.string()), - outputs: z.object({ result: z.object({ type: z.string() }) }).optional(), - }); - - const form = useForm<z.infer<typeof FormSchema>>({ - defaultValues: values, - resolver: zodResolver(FormSchema), - }); - - const method = useWatch({ control: form.control, name: 'method' }); - - const isSplit = method === StringTransformMethod.Split; - - const outputList = useMemo(() => { - return transferOutputs(values.outputs); - }, [values.outputs]); - - const handleMethodChange = useCallback( - (value: StringTransformMethod) => { - const isMerge = value === StringTransformMethod.Merge; - const outputs = { - ...initialStringTransformValues.outputs, - result: { - type: isMerge ? 'string' : 'Array<string>', - }, - }; - form.setValue('outputs', outputs); - form.setValue( - 'delimiters', - isMerge ? StringTransformDelimiter.Comma : [], - ); - }, - [form], - ); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper> - <FormContainer> - <FormField - control={form.control} - name="method" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.method')}</FormLabel> - <FormControl> - <RAGFlowSelect - {...field} - options={Object.values(StringTransformMethod).map( - (val) => ({ label: t('flow.' + val), value: val }), - )} - onChange={(value) => { - handleMethodChange(value); - field.onChange(value); - }} - ></RAGFlowSelect> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - {isSplit && ( - <QueryVariable - label={<FormLabel>split_ref</FormLabel>} - name="split_ref" - ></QueryVariable> - )} - {isSplit || ( - <FormField - control={form.control} - name="script" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.script')}</FormLabel> - <FormControl> - <PromptEditor {...field} showToolbar={false}></PromptEditor> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - )} - <FormField - control={form.control} - name="delimiters" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flow.delimiters')}</FormLabel> - <FormControl> - {isSplit ? ( - <MultiSelect - options={DelimiterOptions} - onValueChange={field.onChange} - defaultValue={field.value as string[]} - variant="inverted" - // {...field} - /> - ) : ( - <RAGFlowSelect - {...field} - options={DelimiterOptions} - ></RAGFlowSelect> - )} - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="outputs" - render={() => <div></div>} - /> - </FormContainer> - </FormWrapper> - <div className="p-5"> - <Output list={outputList}></Output> - </div> - </Form> - ); -} - -export default memo(StringTransformForm); diff --git a/web/src/pages/data-flow/form/string-transform-form/use-values.ts b/web/src/pages/data-flow/form/string-transform-form/use-values.ts deleted file mode 100644 index d4596e339..000000000 --- a/web/src/pages/data-flow/form/string-transform-form/use-values.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { isEmpty } from 'lodash'; -import { useMemo } from 'react'; -import { - initialStringTransformValues, - StringTransformMethod, -} from '../../constant'; - -function transferDelimiters(formData: typeof initialStringTransformValues) { - return formData.method === StringTransformMethod.Merge - ? formData.delimiters[0] - : formData.delimiters; -} - -export function useValues(node?: RAGFlowNodeType) { - const values = useMemo(() => { - const formData = node?.data?.form; - - if (isEmpty(formData)) { - return { - ...initialStringTransformValues, - delimiters: transferDelimiters(formData), - }; - } - - return { - ...formData, - delimiters: transferDelimiters(formData), - }; - }, [node?.data?.form]); - - return values; -} diff --git a/web/src/pages/data-flow/form/string-transform-form/use-watch-form-change.ts b/web/src/pages/data-flow/form/string-transform-form/use-watch-form-change.ts deleted file mode 100644 index c5b7841f2..000000000 --- a/web/src/pages/data-flow/form/string-transform-form/use-watch-form-change.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect } from 'react'; -import { UseFormReturn, useWatch } from 'react-hook-form'; -import { StringTransformMethod } from '../../constant'; -import useGraphStore from '../../store'; - -export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { - 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; - - if ( - values.delimiters !== undefined && - values.method === StringTransformMethod.Merge - ) { - nextValues.delimiters = [values.delimiters]; - } - - updateNodeForm(id, nextValues); - } - }, [form?.formState.isDirty, id, updateNodeForm, values]); -} diff --git a/web/src/pages/data-flow/form/switch-form/index.tsx b/web/src/pages/data-flow/form/switch-form/index.tsx deleted file mode 100644 index b6dcf45bd..000000000 --- a/web/src/pages/data-flow/form/switch-form/index.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import { FormContainer } from '@/components/form-container'; -import { IconFont } from '@/components/icon-font'; -import { SelectWithSearch } from '@/components/originui/select-with-search'; -import { BlockButton, Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from '@/components/ui/form'; -import { RAGFlowSelect } from '@/components/ui/select'; -import { Separator } from '@/components/ui/separator'; -import { Textarea } from '@/components/ui/textarea'; -import { cn } from '@/lib/utils'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { t } from 'i18next'; -import { toLower } from 'lodash'; -import { X } from 'lucide-react'; -import { memo, useCallback, useMemo } from 'react'; -import { useFieldArray, useForm, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; -import { - SwitchLogicOperatorOptions, - SwitchOperatorOptions, - VariableType, -} from '../../constant'; -import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; -import { IOperatorForm } from '../../interface'; -import { FormWrapper } from '../components/form-wrapper'; -import { useValues } from './use-values'; -import { useWatchFormChange } from './use-watch-change'; - -const ConditionKey = 'conditions'; -const ItemKey = 'items'; - -type ConditionCardsProps = { - name: string; - removeParent(index: number): void; - parentIndex: number; - parentLength: number; -} & IOperatorForm; - -export const LogicalOperatorIcon = function OperatorIcon({ - icon, - value, -}: Omit<(typeof SwitchOperatorOptions)[0], 'label'>) { - if (typeof icon === 'string') { - return ( - <IconFont - name={icon} - className={cn('size-4', { - 'rotate-180': value === '>', - })} - ></IconFont> - ); - } - return icon; -}; - -export function useBuildSwitchOperatorOptions() { - const { t } = useTranslation(); - - const switchOperatorOptions = useMemo(() => { - return SwitchOperatorOptions.map((x) => ({ - value: x.value, - icon: ( - <LogicalOperatorIcon - icon={x.icon} - value={x.value} - ></LogicalOperatorIcon> - ), - label: t(`flow.switchOperatorOptions.${x.label}`), - })); - }, [t]); - - return switchOperatorOptions; -} - -function ConditionCards({ - name: parentName, - parentIndex, - removeParent, - parentLength, -}: ConditionCardsProps) { - const form = useFormContext(); - - const nextOptions = useBuildQueryVariableOptions(); - - const finalOptions = useMemo(() => { - return nextOptions.map((x) => { - return { - ...x, - options: x.options.filter( - (y) => !toLower(y.type).includes(VariableType.Array), - ), - }; - }); - }, [nextOptions]); - - const switchOperatorOptions = useBuildSwitchOperatorOptions(); - - const name = `${parentName}.${ItemKey}`; - - const { fields, remove, append } = useFieldArray({ - name: name, - control: form.control, - }); - - const handleRemove = useCallback( - (index: number) => () => { - remove(index); - if (parentIndex !== 0 && index === 0 && parentLength === 1) { - removeParent(parentIndex); - } - }, - [parentIndex, parentLength, remove, removeParent], - ); - - return ( - <section className="flex-1 space-y-2.5 min-w-0"> - {fields.map((field, index) => { - return ( - <div key={field.id} className="flex"> - <Card - className={cn( - 'relative bg-transparent border-input-border border flex-1 min-w-0', - { - 'before:w-10 before:absolute before:h-[1px] before:bg-input-border before:top-1/2 before:-left-10': - fields.length > 1 && - (index === 0 || index === fields.length - 1), - }, - )} - > - <section className="p-2 bg-bg-card flex justify-between items-center"> - <FormField - control={form.control} - name={`${name}.${index}.cpn_id`} - render={({ field }) => ( - <FormItem className="flex-1 min-w-0"> - <FormControl> - <SelectWithSearch - {...field} - options={finalOptions} - triggerClassName="text-accent-primary bg-transparent border-none truncate" - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <div className="flex items-center"> - <Separator orientation="vertical" className="h-2.5" /> - <FormField - control={form.control} - name={`${name}.${index}.operator`} - render={({ field }) => ( - <FormItem> - <FormControl> - <RAGFlowSelect - {...field} - options={switchOperatorOptions} - onlyShowSelectedIcon - triggerClassName="w-30 bg-transparent border-none" - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - </section> - <CardContent className="p-4 "> - <FormField - control={form.control} - name={`${name}.${index}.value`} - render={({ field }) => ( - <FormItem> - <FormControl> - <Textarea {...field} className="bg-transparent" /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - <Button variant={'ghost'} onClick={handleRemove(index)}> - <X /> - </Button> - </div> - ); - })} - <div className="pr-9"> - <BlockButton - className="mt-6" - onClick={() => append({ operator: switchOperatorOptions[0].value })} - > - {t('common.add')} - </BlockButton> - </div> - </section> - ); -} - -function SwitchForm({ node }: IOperatorForm) { - const { t } = useTranslation(); - const values = useValues(node); - const switchOperatorOptions = useBuildSwitchOperatorOptions(); - - const FormSchema = z.object({ - conditions: z.array( - z - .object({ - logical_operator: z.string(), - items: z - .array( - z.object({ - cpn_id: z.string(), - operator: z.string(), - value: z.string().optional(), - }), - ) - .optional(), - to: z.array(z.string()).optional(), - }) - .optional(), - ), - }); - - const form = useForm({ - defaultValues: values, - resolver: zodResolver(FormSchema), - }); - - const { fields, remove, append } = useFieldArray({ - name: ConditionKey, - control: form.control, - }); - - const switchLogicOperatorOptions = useMemo(() => { - return SwitchLogicOperatorOptions.map((x) => ({ - value: x, - label: t(`flow.switchLogicOperatorOptions.${x}`), - })); - }, [t]); - - useWatchFormChange(node?.id, form); - - return ( - <Form {...form}> - <FormWrapper> - {fields.map((field, index) => { - const name = `${ConditionKey}.${index}`; - const conditions: Array<any> = form.getValues(`${name}.${ItemKey}`); - const conditionLength = conditions.length; - return ( - <FormContainer key={field.id} className=""> - <div className="flex justify-between items-center"> - <section> - <span>{index === 0 ? 'IF' : 'ELSEIF'}</span> - <div className="text-text-secondary">Case {index + 1}</div> - </section> - {index !== 0 && ( - <Button - variant={'secondary'} - className="-translate-y-1" - onClick={() => remove(index)} - > - {t('common.remove')} <X /> - </Button> - )} - </div> - <section className="flex gap-2 !mt-2 relative"> - {conditionLength > 1 && ( - <section className="flex flex-col w-[72px]"> - <div className="relative w-1 flex-1 before:absolute before:w-[1px] before:bg-input-border before:top-20 before:bottom-0 before:left-10"></div> - <FormField - control={form.control} - name={`${ConditionKey}.${index}.logical_operator`} - render={({ field }) => ( - <FormItem> - <FormControl> - <RAGFlowSelect - {...field} - options={switchLogicOperatorOptions} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <div className="relative w-1 flex-1 before:absolute before:w-[1px] before:bg-input-border before:top-0 before:bottom-36 before:left-10"></div> - </section> - )} - <ConditionCards - name={name} - removeParent={remove} - parentIndex={index} - parentLength={fields.length} - ></ConditionCards> - </section> - </FormContainer> - ); - })} - <BlockButton - onClick={() => - append({ - logical_operator: SwitchLogicOperatorOptions[0], - [ItemKey]: [ - { - operator: switchOperatorOptions[0].value, - }, - ], - to: [], - }) - } - > - {t('common.add')} - </BlockButton> - </FormWrapper> - </Form> - ); -} - -export default memo(SwitchForm); diff --git a/web/src/pages/data-flow/form/switch-form/use-values.ts b/web/src/pages/data-flow/form/switch-form/use-values.ts deleted file mode 100644 index 990bd0124..000000000 --- a/web/src/pages/data-flow/form/switch-form/use-values.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { isEmpty } from 'lodash'; -import { useMemo } from 'react'; -import { initialSwitchValues } from '../../constant'; - -export function useValues(node?: RAGFlowNodeType) { - const values = useMemo(() => { - const formData = node?.data?.form; - if (isEmpty(formData)) { - return initialSwitchValues; - } - - return formData; - }, [node]); - - return values; -} diff --git a/web/src/pages/data-flow/form/switch-form/use-watch-change.ts b/web/src/pages/data-flow/form/switch-form/use-watch-change.ts deleted file mode 100644 index 0c8b8259e..000000000 --- a/web/src/pages/data-flow/form/switch-form/use-watch-change.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ISwitchCondition } from '@/interfaces/database/agent'; -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 - console.log('🚀 ~ useWatchFormChange ~ values:', form?.formState.isDirty); - if (id) { - values = form?.getValues() || {}; - let nextValues: any = { - ...values, - conditions: - values?.conditions?.map((x: ISwitchCondition) => ({ ...x })) ?? [], // Changing the form value with useFieldArray does not change the array reference - }; - - updateNodeForm(id, nextValues); - } - }, [form?.formState.isDirty, id, updateNodeForm, values]); -} diff --git a/web/src/pages/data-flow/form/tokenizer-form/index.tsx b/web/src/pages/data-flow/form/tokenizer-form/index.tsx index b48581390..1b4975db9 100644 --- a/web/src/pages/data-flow/form/tokenizer-form/index.tsx +++ b/web/src/pages/data-flow/form/tokenizer-form/index.tsx @@ -1,94 +1,53 @@ -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 { RAGFlowFormItem } from '@/components/ragflow-form'; +import { SliderInputFormField } from '@/components/slider-input-form-field'; +import { Form } from '@/components/ui/form'; +import { MultiSelect } from '@/components/ui/multi-select'; +import { buildOptions } from '@/utils/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { memo } from 'react'; -import { useForm, useFormContext } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { initialChunkerValues } from '../../constant'; +import { + initialTokenizerValues, + TokenizerFields, + TokenizerSearchMethod, +} 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(initialChunkerValues.outputs); - -export const GoogleFormPartialSchema = { - api_key: z.string(), - country: z.string(), - language: z.string(), -}; +const outputList = buildOutputList(initialTokenizerValues.outputs); export const FormSchema = z.object({ - ...GoogleFormPartialSchema, - q: z.string(), - start: z.number(), - num: z.number(), + search_method: z.array(z.string()).min(1), + filename_embd_weight: z.number(), + fields: z.string(), }); -export function GoogleFormWidgets() { - const form = useFormContext(); - const { t } = useTranslate('flow'); - - return ( - <> - <FormField - control={form.control} - name={`country`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('country')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={GoogleCountryOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`language`} - render={({ field }) => ( - <FormItem className="flex-1"> - <FormLabel>{t('language')}</FormLabel> - <FormControl> - <SelectWithSearch - {...field} - options={GoogleLanguageOptions} - ></SelectWithSearch> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </> - ); -} - const TokenizerForm = ({ node }: INextOperatorForm) => { - const { t } = useTranslate('flow'); - const defaultValues = useFormValues(initialChunkerValues, node); + const { t } = useTranslation(); + const defaultValues = useFormValues(initialTokenizerValues, node); + + const SearchMethodOptions = buildOptions( + TokenizerSearchMethod, + t, + `dataflow.tokenizerSearchMethodOptions`, + ); + const FieldsOptions = buildOptions( + TokenizerFields, + t, + 'dataflow.tokenizerFieldsOptions', + ); const form = useForm<z.infer<typeof FormSchema>>({ defaultValues, resolver: zodResolver(FormSchema), + mode: 'onChange', }); useWatchFormChange(node?.id, form); @@ -96,39 +55,28 @@ const TokenizerForm = ({ node }: INextOperatorForm) => { return ( <Form {...form}> <FormWrapper> - <FormContainer> - <QueryVariable name="q"></QueryVariable> - </FormContainer> - <FormContainer> - <ApiKeyField placeholder={t('apiKeyPlaceholder')}></ApiKeyField> - <FormField - control={form.control} - name={`start`} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flowStart')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`num`} - render={({ field }) => ( - <FormItem> - <FormLabel>{t('flowNum')}</FormLabel> - <FormControl> - <NumberInput {...field} className="w-full"></NumberInput> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <GoogleFormWidgets></GoogleFormWidgets> - </FormContainer> + <RAGFlowFormItem + name="search_method" + label={t('dataflow.searchMethod')} + > + {(field) => ( + <MultiSelect + options={SearchMethodOptions} + onValueChange={field.onChange} + defaultValue={field.value} + variant="inverted" + /> + )} + </RAGFlowFormItem> + <SliderInputFormField + name="filename_embd_weight" + label={t('dataflow.filenameEmbeddingWeight')} + max={0.5} + step={0.01} + ></SliderInputFormField> + <RAGFlowFormItem name="fields" label={t('dataflow.fields')}> + {(field) => <SelectWithSearch options={FieldsOptions} {...field} />} + </RAGFlowFormItem> </FormWrapper> <div className="p-5"> <Output list={outputList}></Output> diff --git a/web/src/pages/data-flow/form/user-fill-up-form/index.tsx b/web/src/pages/data-flow/form/user-fill-up-form/index.tsx deleted file mode 100644 index 68def5601..000000000 --- a/web/src/pages/data-flow/form/user-fill-up-form/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { Collapse } from '@/components/collapse'; -import { Button } from '@/components/ui/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -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 { Plus } from 'lucide-react'; -import { memo } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; -import { BeginQuery, INextOperatorForm } from '../../interface'; -import { ParameterDialog } from '../begin-form/parameter-dialog'; -import { QueryTable } from '../begin-form/query-table'; -import { useEditQueryRecord } from '../begin-form/use-edit-query'; -import { Output } from '../components/output'; -import { useValues } from './use-values'; -import { useWatchFormChange } from './use-watch-change'; - -function UserFillUpForm({ node }: INextOperatorForm) { - const { t } = useTranslation(); - - const values = useValues(node); - - const FormSchema = z.object({ - enable_tips: z.boolean().optional(), - tips: z.string().trim().optional(), - 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: BeginQuery[] = useWatch({ - control: form.control, - name: 'inputs', - }); - - const outputList = inputs?.map((item) => ({ - title: item.name, - type: item.type, - })); - - const { - ok, - currentRecord, - visible, - hideModal, - showModal, - otherThanCurrentQuery, - handleDeleteRecord, - } = useEditQueryRecord({ - form, - node, - }); - - return ( - <section className="px-5 space-y-5"> - <Form {...form}> - <FormField - control={form.control} - name={'enable_tips'} - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('flow.openingSwitchTip')}> - {t('flow.guidingQuestion')} - </FormLabel> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name={'tips'} - render={({ field }) => ( - <FormItem> - <FormLabel tooltip={t('chat.setAnOpenerTip')}> - {t('flow.msg')} - </FormLabel> - <FormControl> - <Textarea - rows={5} - {...field} - placeholder={t('common.pleaseInput')} - ></Textarea> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* Create a hidden field to make Form instance record this */} - <FormField - control={form.control} - name={'inputs'} - render={() => <div></div>} - /> - <Collapse - title={ - <div> - {t('flow.input')} - <FormTooltip tooltip={t('flow.beginInputTip')}></FormTooltip> - </div> - } - rightContent={ - <Button - variant={'ghost'} - onClick={(e) => { - e.preventDefault(); - showModal(); - }} - > - <Plus /> - </Button> - } - > - <QueryTable - data={inputs} - showModal={showModal} - deleteRecord={handleDeleteRecord} - ></QueryTable> - </Collapse> - - {visible && ( - <ParameterDialog - hideModal={hideModal} - initialValue={currentRecord} - otherThanCurrentQuery={otherThanCurrentQuery} - submit={ok} - ></ParameterDialog> - )} - </Form> - <Output list={outputList}></Output> - </section> - ); -} - -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 deleted file mode 100644 index 0af1c78c3..000000000 --- a/web/src/pages/data-flow/form/user-fill-up-form/use-values.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index bdf4b9a91..000000000 --- a/web/src/pages/data-flow/form/user-fill-up-form/use-watch-change.ts +++ /dev/null @@ -1,35 +0,0 @@ -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<Record<string, Omit<BeginQuery, 'key'>>>((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/hooks/use-add-node.ts b/web/src/pages/data-flow/hooks/use-add-node.ts index 7e0ddf584..2b3e23c9d 100644 --- a/web/src/pages/data-flow/hooks/use-add-node.ts +++ b/web/src/pages/data-flow/hooks/use-add-node.ts @@ -1,7 +1,6 @@ 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'; @@ -9,30 +8,13 @@ import { NodeHandleId, NodeMap, Operator, - initialAgentValues, initialBeginValues, - initialCategorizeValues, - initialChunkerValues, - initialCodeValues, - initialConcentratorValues, - initialCrawlerValues, - initialEmailValues, - initialExeSqlValues, - initialInvokeValues, - initialIterationStartValues, - initialIterationValues, - initialKeywordExtractValues, - initialMessageValues, + initialExtractorValues, + initialHierarchicalMergerValues, initialNoteValues, initialParserValues, - initialRelevantValues, - initialRetrievalValues, - initialRewriteQuestionValues, - initialStringTransformValues, - initialSwitchValues, + initialSplitterValues, initialTokenizerValues, - initialUserFillUpValues, - initialWaitingDialogueValues, } from '../constant'; import useGraphStore from '../store'; import { @@ -40,61 +22,30 @@ import { 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 { t } = useTranslation(); 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.ExeSQL]: initialExeSqlValues, - [Operator.Switch]: initialSwitchValues, - [Operator.Concentrator]: initialConcentratorValues, [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.UserFillUp]: initialUserFillUpValues, - [Operator.StringTransform]: initialStringTransformValues, [Operator.Parser]: initialParserValues, - [Operator.Chunker]: initialChunkerValues, [Operator.Tokenizer]: initialTokenizerValues, + [Operator.Splitter]: initialSplitterValues, + [Operator.HierarchicalMerger]: initialHierarchicalMergerValues, + [Operator.Extractor]: { + ...initialExtractorValues, + llm_id: llmId, + sys_prompt: t('dataflow.prompts.system.summary'), + prompts: t('dataflow.prompts.user.summary'), + }, }; - }, [llmId]); + }, [llmId, t]); const initializeOperatorParams = useCallback( - (operatorName: Operator, position: Position) => { + (operatorName: Operator) => { const initialValues = initialFormValuesMap[operatorName]; - if (isBottomSubAgent(operatorName, position)) { - return { - ...initialValues, - description: t('flow.descriptionMessage'), - user_prompt: t('flow.userPromptDefaultValue'), - }; - } return initialValues; }, @@ -171,95 +122,17 @@ function useAddChildEdge() { return { addChildEdge }; } -function useAddToolNode() { - const { nodes, edges, addEdge, getNode, addNode } = useGraphStore( - (state) => state, - ); - - const addToolNode = useCallback( - (newNode: Node<any>, 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<HTMLElement>, 'clientX' | 'clientY' >; export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { - const { edges, nodes, addEdge, addNode, getNode } = useGraphStore( - (state) => state, - ); + const { nodes, addNode } = 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<ReactFlowInstance<any, any>>(); @@ -277,7 +150,6 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { ) => (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 @@ -308,112 +180,30 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { getNodeName(type), nodes, ), - form: initializeOperatorParams(type as Operator, params.position), + form: initializeOperatorParams(type as Operator), }, 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<any> = { - 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, - }); - } + 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, ], ); diff --git a/web/src/pages/data-flow/hooks/use-before-delete.tsx b/web/src/pages/data-flow/hooks/use-before-delete.tsx index d08333c86..bbeeaae10 100644 --- a/web/src/pages/data-flow/hooks/use-before-delete.tsx +++ b/web/src/pages/data-flow/hooks/use-before-delete.tsx @@ -1,17 +1,12 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { Node, OnBeforeDelete } from '@xyflow/react'; +import { OnBeforeDelete } from '@xyflow/react'; import { Operator } from '../constant'; import useGraphStore from '../store'; -import { deleteAllDownstreamAgentsAndTool } from '../utils/delete-node'; -const UndeletableNodes = [Operator.Begin, Operator.IterationStart]; +const UndeletableNodes = [Operator.Begin]; export function useBeforeDelete() { - const { getOperatorTypeFromId, getNode } = useGraphStore((state) => state); - - const agentPredicate = (node: Node) => { - return getOperatorTypeFromId(node.id) === Operator.Agent; - }; + const { getOperatorTypeFromId } = useGraphStore((state) => state); const handleBeforeDelete: OnBeforeDelete<RAGFlowNodeType> = async ({ nodes, // Nodes to be deleted @@ -23,13 +18,6 @@ export function useBeforeDelete() { return false; } - if ( - operatorType === Operator.IterationStart && - !nodes.some((x) => x.id === node.parentId) - ) { - return false; - } - return true; }); @@ -51,27 +39,6 @@ export function useBeforeDelete() { 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, diff --git a/web/src/pages/data-flow/hooks/use-build-options.tsx b/web/src/pages/data-flow/hooks/use-build-options.tsx new file mode 100644 index 000000000..d1214f729 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-build-options.tsx @@ -0,0 +1,19 @@ +import { buildNodeOutputOptions } from '@/utils/canvas-util'; +import { useMemo } from 'react'; +import { Operator } from '../constant'; +import OperatorIcon from '../operator-icon'; +import useGraphStore from '../store'; + +export function useBuildNodeOutputOptions(nodeId?: string) { + const nodes = useGraphStore((state) => state.nodes); + const edges = useGraphStore((state) => state.edges); + + return useMemo(() => { + return buildNodeOutputOptions({ + nodes, + edges, + nodeId, + Icon: ({ name }) => <OperatorIcon name={name as Operator}></OperatorIcon>, + }); + }, [edges, nodeId, nodes]); +} diff --git a/web/src/pages/data-flow/hooks/use-cancel-dataflow.ts b/web/src/pages/data-flow/hooks/use-cancel-dataflow.ts new file mode 100644 index 000000000..96705a0f8 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-cancel-dataflow.ts @@ -0,0 +1,21 @@ +import { useCancelDataflow } from '@/hooks/use-agent-request'; +import { useCallback } from 'react'; + +export function useCancelCurrentDataflow({ + messageId, + stopFetchTrace, +}: { + messageId: string; + stopFetchTrace(): void; +}) { + const { cancelDataflow } = useCancelDataflow(); + + const handleCancel = useCallback(async () => { + const code = await cancelDataflow(messageId); + if (code === 0) { + stopFetchTrace(); + } + }, [cancelDataflow, messageId, stopFetchTrace]); + + return { handleCancel }; +} 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 index 61a5653d7..da7316a84 100644 --- a/web/src/pages/data-flow/hooks/use-change-node-name.ts +++ b/web/src/pages/data-flow/hooks/use-change-node-name.ts @@ -9,7 +9,6 @@ import { useMemo, useState, } from 'react'; -import { Operator } from '../constant'; import useGraphStore from '../store'; import { getAgentNodeTools } from '../utils'; @@ -77,13 +76,10 @@ export const useHandleNodeNameChange = ({ data: any; }) => { const [name, setName] = useState<string>(''); - const { updateNodeName, nodes, getOperatorTypeFromId } = useGraphStore( - (state) => state, - ); + const { updateNodeName, nodes } = useGraphStore((state) => state); const previousName = data?.name; - const isToolNode = getOperatorTypeFromId(id) === Operator.Tool; - const { handleToolNameBlur, previousToolName } = useHandleTooNodeNameChange({ + const { previousToolName } = useHandleTooNodeNameChange({ id, name, setName, @@ -109,12 +105,12 @@ export const useHandleNodeNameChange = ({ }, []); useEffect(() => { - setName(isToolNode ? previousToolName : previousName); - }, [isToolNode, previousName, previousToolName]); + setName(previousName); + }, [previousName, previousToolName]); return { name, - handleNameBlur: isToolNode ? handleToolNameBlur : handleNameBlur, + handleNameBlur: 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 deleted file mode 100644 index 42b0533cb..000000000 --- a/web/src/pages/data-flow/hooks/use-chat-logic.ts +++ /dev/null @@ -1,60 +0,0 @@ -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<string, BeginQuery>; - 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<string, BeginQuery>; - }, []); - - 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-download-output.ts b/web/src/pages/data-flow/hooks/use-download-output.ts new file mode 100644 index 000000000..8e02105e8 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-download-output.ts @@ -0,0 +1,38 @@ +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { ITraceData } from '@/interfaces/database/agent'; +import { downloadJsonFile } from '@/utils/file-util'; +import { get, isEmpty } from 'lodash'; +import { useCallback } from 'react'; + +export function findEndOutput(list?: ITraceData[]) { + if (Array.isArray(list)) { + const trace = list.find((x) => x.component_id === 'END')?.trace; + + const str = get(trace, '0.message'); + + try { + if (!isEmpty(str)) { + const json = JSON.parse(str); + return json; + } + } catch (error) {} + } +} + +export function isEndOutputEmpty(list?: ITraceData[]) { + return isEmpty(findEndOutput(list)); +} +export function useDownloadOutput(data?: ITraceData[]) { + const { data: agent } = useFetchAgent(); + + const handleDownloadJson = useCallback(() => { + const output = findEndOutput(data); + if (!isEndOutputEmpty(data)) { + downloadJsonFile(output, `${agent.title}.json`); + } + }, [agent.title, data]); + + return { + handleDownloadJson, + }; +} diff --git a/web/src/pages/data-flow/hooks/use-export-json.ts b/web/src/pages/data-flow/hooks/use-export-json.ts index 1efe6bb50..87a811cf5 100644 --- a/web/src/pages/data-flow/hooks/use-export-json.ts +++ b/web/src/pages/data-flow/hooks/use-export-json.ts @@ -1,71 +1,17 @@ -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-log.ts b/web/src/pages/data-flow/hooks/use-fetch-log.ts new file mode 100644 index 000000000..e8918bdad --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-fetch-log.ts @@ -0,0 +1,56 @@ +import { useFetchMessageTrace } from '@/hooks/use-agent-request'; +import { isEmpty } from 'lodash'; +import { useCallback, useEffect, useMemo } from 'react'; + +export function useFetchLog(logSheetVisible: boolean) { + const { + setMessageId, + data, + loading, + messageId, + setISStopFetchTrace, + isStopFetchTrace, + } = useFetchMessageTrace(); + + const isCompleted = useMemo(() => { + if (Array.isArray(data)) { + const latest = data?.at(-1); + return ( + latest?.component_id === 'END' && !isEmpty(latest?.trace[0].message) + ); + } + return false; + }, [data]); + + const isLogEmpty = !data || !data.length; + + const stopFetchTrace = useCallback(() => { + setISStopFetchTrace(true); + }, [setISStopFetchTrace]); + + // cancel request + useEffect(() => { + if (isCompleted) { + stopFetchTrace(); + } + }, [isCompleted, stopFetchTrace]); + + useEffect(() => { + if (logSheetVisible) { + setISStopFetchTrace(false); + } + }, [logSheetVisible, setISStopFetchTrace]); + + return { + logs: data, + isLogEmpty, + isCompleted, + loading, + isParsing: !isLogEmpty && !isCompleted && !isStopFetchTrace, + messageId, + setMessageId, + stopFetchTrace, + }; +} + +export type UseFetchLogReturnType = ReturnType<typeof useFetchLog>; 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 deleted file mode 100644 index e4c5aed5a..000000000 --- a/web/src/pages/data-flow/hooks/use-find-mcp-by-id.ts +++ /dev/null @@ -1,12 +0,0 @@ -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-get-begin-query.tsx b/web/src/pages/data-flow/hooks/use-get-begin-query.tsx deleted file mode 100644 index 83cda2078..000000000 --- a/web/src/pages/data-flow/hooks/use-get-begin-query.tsx +++ /dev/null @@ -1,317 +0,0 @@ -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<string[]>((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<string, any> = {}, - 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, - <OperatorIcon name={x.data.label as Operator} />, - ), - })); - }, [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: <span>{t('flow.beginInput')}</span>, - title: t('flow.beginInput'), - options: inputs.map((x) => ({ - label: x.name, - parentLabel: <span>{t('flow.beginInput')}</span>, - icon: <OperatorIcon name={Operator.Begin} className="block" />, - 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: <OperatorIcon name={Operator.Begin} className="block" />, - parentLabel: <span>{t('flow.beginInput')}</span>, - type: Array.isArray(value) - ? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '<file>' : ''}` - : 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: <span>Component Output</span>, - 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<DefaultOptionType[]>((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<DefaultOptionType[]>((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 deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/src/pages/data-flow/hooks/use-run-dataflow.ts b/web/src/pages/data-flow/hooks/use-run-dataflow.ts new file mode 100644 index 000000000..e49f1ac56 --- /dev/null +++ b/web/src/pages/data-flow/hooks/use-run-dataflow.ts @@ -0,0 +1,59 @@ +import message from '@/components/ui/message'; +import { useSendMessageBySSE } from '@/hooks/use-send-message'; +import api from '@/utils/api'; +import { get } from 'lodash'; +import { useCallback, useContext } from 'react'; +import { useParams } from 'umi'; +import { LogContext } from '../context'; +import { useSaveGraphBeforeOpeningDebugDrawer } from './use-save-graph'; + +export function useRunDataflow( + showLogSheet: () => void, + hideRunOrChatDrawer: () => void, +) { + const { send } = useSendMessageBySSE(api.runCanvas); + const { id } = useParams(); + const { setMessageId, setUploadedFileData } = useContext(LogContext); + + const { handleRun: saveGraph, loading } = + useSaveGraphBeforeOpeningDebugDrawer(showLogSheet!); + + const run = useCallback( + async (fileResponseData: Record<string, any>) => { + const success = await saveGraph(); + if (!success) return; + const res = await send({ + id, + query: '', + session_id: null, + files: [fileResponseData.file], + }); + + if (res && res?.response.status === 200 && get(res, 'data.code') === 0) { + // fetch canvas + hideRunOrChatDrawer(); + setUploadedFileData(fileResponseData.file); + const msgId = get(res, 'data.data.message_id'); + if (msgId) { + setMessageId(msgId); + } + + return msgId; + } else { + message.error(get(res, 'data.message', '')); + } + }, + [ + hideRunOrChatDrawer, + id, + saveGraph, + send, + setMessageId, + setUploadedFileData, + ], + ); + + return { run, loading: loading }; +} + +export type RunDataflowType = ReturnType<typeof useRunDataflow>; diff --git a/web/src/pages/data-flow/hooks/use-save-graph.ts b/web/src/pages/data-flow/hooks/use-save-graph.ts index 8ce4dc15e..322e8b80e 100644 --- a/web/src/pages/data-flow/hooks/use-save-graph.ts +++ b/web/src/pages/data-flow/hooks/use-save-graph.ts @@ -1,8 +1,4 @@ -import { - useFetchAgent, - useResetAgent, - useSetAgent, -} from '@/hooks/use-agent-request'; +import { useFetchAgent, useSetAgent } from '@/hooks/use-agent-request'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { formatDate } from '@/utils/date'; import { useDebounceEffect } from 'ahooks'; @@ -22,6 +18,7 @@ export const useSaveGraph = (showMessage: boolean = true) => { return setAgent({ id, title: data.title, + canvas_category: data.canvas_category, dsl: buildDslData(currentNodes), }); }, @@ -33,21 +30,22 @@ export const useSaveGraph = (showMessage: boolean = true) => { export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { const { saveGraph, loading } = useSaveGraph(); - const { resetAgent } = useResetAgent(); + // 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(); + // const resetRet = await resetAgent(); // After resetting, all previous messages will be cleared. - if (resetRet?.code === 0) { - show(); - } + // if (resetRet?.code === 0) { + show(); + // } } + return saveRet?.code === 0; }, - [saveGraph, resetAgent, show], + [saveGraph, show], ); return { handleRun, loading }; diff --git a/web/src/pages/data-flow/hooks/use-show-drawer.tsx b/web/src/pages/data-flow/hooks/use-show-drawer.tsx index 6789e86ba..82a9bec85 100644 --- a/web/src/pages/data-flow/hooks/use-show-drawer.tsx +++ b/web/src/pages/data-flow/hooks/use-show-drawer.tsx @@ -2,10 +2,9 @@ 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 { BeginId, 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 = () => { @@ -13,7 +12,6 @@ export const useShowFormDrawer = () => { clickedNodeId: clickNodeId, setClickedNodeId, getNode, - setClickedToolId, } = useGraphStore((state) => state); const { visible: formDrawerVisible, @@ -23,16 +21,14 @@ export const useShowFormDrawer = () => { const handleShow = useCallback( (e: React.MouseEvent<Element>, nodeId: string) => { - const tool = get(e.target, 'dataset.tool'); // TODO: Operator type judgment should be used - if (nodeId.startsWith(Operator.Tool) && !tool) { + if (nodeId === BeginId) { return; } setClickedNodeId(nodeId); - setClickedToolId(tool); showFormDrawer(); }, - [setClickedNodeId, setClickedToolId, showFormDrawer], + [setClickedNodeId, showFormDrawer], ); return { @@ -87,26 +83,12 @@ export function useShowDrawer({ } = useShowSingleDebugDrawer(); const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } = useShowFormDrawer(); - const inputs = useGetBeginNodeDataInputs(); useEffect(() => { if (drawerVisible) { - if (inputs.length > 0) { - showRunModal(); - hideChatModal(); - } else { - showChatModal(); - hideRunModal(); - } + showRunModal(); } - }, [ - hideChatModal, - hideRunModal, - showChatModal, - showRunModal, - drawerVisible, - inputs, - ]); + }, [hideChatModal, hideRunModal, showChatModal, showRunModal, drawerVisible]); const hideRunOrChatDrawer = useCallback(() => { hideChatModal(); 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 index 534c2e2a9..1f85b107f 100644 --- a/web/src/pages/data-flow/hooks/use-watch-form-change.ts +++ b/web/src/pages/data-flow/hooks/use-watch-form-change.ts @@ -4,15 +4,12 @@ import useGraphStore from '../store'; export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { 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); + updateNodeForm(id, values); } - }, [form?.formState.isDirty, id, updateNodeForm, values]); + }, [id, updateNodeForm, values]); } diff --git a/web/src/pages/data-flow/index.tsx b/web/src/pages/data-flow/index.tsx index 10798b5e8..cdca3ebae 100644 --- a/web/src/pages/data-flow/index.tsx +++ b/web/src/pages/data-flow/index.tsx @@ -21,26 +21,27 @@ import { ReactFlowProvider } from '@xyflow/react'; import { ChevronDown, CirclePlay, - Download, History, LaptopMinimalCheck, Settings, Upload, } from 'lucide-react'; -import { ComponentPropsWithoutRef, useCallback } from 'react'; +import { ComponentPropsWithoutRef, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import DataFlowCanvas from './canvas'; import { DropdownProvider } from './canvas/context'; +import { LogContext } from './context'; +import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow'; import { useHandleExportOrImportJsonFile } from './hooks/use-export-json'; import { useFetchDataOnMount } from './hooks/use-fetch-data'; -import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query'; +import { useFetchLog } from './hooks/use-fetch-log'; import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer, useWatchAgentChange, } from './hooks/use-save-graph'; +import { LogSheet } from './log-sheet'; import { SettingDialog } from './setting-dialog'; -import { UploadAgentDialog } from './upload-agent-dialog'; import { useAgentHistoryManager } from './use-agent-history-manager'; import { VersionDialog } from './version-dialog'; @@ -64,24 +65,12 @@ export default function DataFlow() { } = useSetModalState(); const { t } = useTranslation(); useAgentHistoryManager(); - const { - handleExportJson, - handleImportJson, - fileUploadVisible, - onFileUploadOk, - hideFileUploadModal, - } = useHandleExportOrImportJsonFile(); + const { handleExportJson } = 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 { handleRun, loading: running } = + useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); + const { visible: versionDialogVisible, hideModal: hideVersionDialog, @@ -94,6 +83,40 @@ export default function DataFlow() { showModal: showSettingDialog, } = useSetModalState(); + const { + visible: logSheetVisible, + showModal: showLogSheet, + hideModal: hideLogSheet, + } = useSetModalState(); + + const { + isParsing, + logs, + messageId, + setMessageId, + isCompleted, + stopFetchTrace, + isLogEmpty, + } = useFetchLog(logSheetVisible); + + const [uploadedFileData, setUploadedFileData] = + useState<Record<string, any>>(); + + const handleRunAgent = useCallback(() => { + if (isParsing) { + // show log sheet + showLogSheet(); + } else { + hideLogSheet(); + handleRun(); + } + }, [handleRun, hideLogSheet, isParsing, showLogSheet]); + + const { handleCancel } = useCancelCurrentDataflow({ + messageId, + stopFetchTrace, + }); + const time = useWatchAgentChange(chatDrawerVisible); return ( @@ -125,15 +148,25 @@ export default function DataFlow() { > <LaptopMinimalCheck /> {t('flow.save')} </ButtonLoading> - <Button variant={'secondary'} onClick={handleRunAgent}> - <CirclePlay /> - {t('flow.run')} - </Button> + <ButtonLoading + variant={'secondary'} + onClick={handleRunAgent} + loading={running} + > + {running || ( + <CirclePlay className={isParsing ? 'animate-spin' : ''} /> + )} + + {isParsing || running ? t('dataflow.running') : t('flow.run')} + </ButtonLoading> <Button variant={'secondary'} onClick={showVersionDialog}> <History /> {t('flow.historyversion')} </Button> - + {/* <Button variant={'secondary'}> + <Send /> + {t('flow.release')} + </Button> */} <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant={'secondary'}> @@ -141,11 +174,6 @@ export default function DataFlow() { </Button> </DropdownMenuTrigger> <DropdownMenuContent> - <AgentDropdownMenuItem onClick={handleImportJson}> - <Download /> - {t('flow.import')} - </AgentDropdownMenuItem> - <DropdownMenuSeparator /> <AgentDropdownMenuItem onClick={handleExportJson}> <Upload /> {t('flow.export')} @@ -159,21 +187,19 @@ export default function DataFlow() { </DropdownMenu> </div> </PageHeader> - <ReactFlowProvider> - <DropdownProvider> - <DataFlowCanvas - drawerVisible={chatDrawerVisible} - hideDrawer={hideChatDrawer} - ></DataFlowCanvas> - </DropdownProvider> - </ReactFlowProvider> - {fileUploadVisible && ( - <UploadAgentDialog - hideModal={hideFileUploadModal} - onOk={onFileUploadOk} - ></UploadAgentDialog> - )} - + <LogContext.Provider + value={{ messageId, setMessageId, setUploadedFileData }} + > + <ReactFlowProvider> + <DropdownProvider> + <DataFlowCanvas + drawerVisible={chatDrawerVisible} + hideDrawer={hideChatDrawer} + showLogSheet={showLogSheet} + ></DataFlowCanvas> + </DropdownProvider> + </ReactFlowProvider> + </LogContext.Provider> {versionDialogVisible && ( <DropdownProvider> <VersionDialog hideModal={hideVersionDialog}></VersionDialog> @@ -182,6 +208,18 @@ export default function DataFlow() { {settingDialogVisible && ( <SettingDialog hideModal={hideSettingDialog}></SettingDialog> )} + {logSheetVisible && ( + <LogSheet + hideModal={hideLogSheet} + isParsing={isParsing} + isCompleted={isCompleted} + isLogEmpty={isLogEmpty} + logs={logs} + handleCancel={handleCancel} + messageId={messageId} + uploadedFileData={uploadedFileData} + ></LogSheet> + )} </section> ); } diff --git a/web/src/pages/data-flow/log-sheet/dataflow-timeline.tsx b/web/src/pages/data-flow/log-sheet/dataflow-timeline.tsx new file mode 100644 index 000000000..25787a2e4 --- /dev/null +++ b/web/src/pages/data-flow/log-sheet/dataflow-timeline.tsx @@ -0,0 +1,137 @@ +import { + Timeline, + TimelineContent, + TimelineHeader, + TimelineIndicator, + TimelineItem, + TimelineSeparator, + TimelineTitle, +} from '@/components/originui/timeline'; +import { Progress } from '@/components/ui/progress'; +import { ITraceData } from '@/interfaces/database/agent'; +import { cn } from '@/lib/utils'; +import { isEmpty } from 'lodash'; +import { File } from 'lucide-react'; +import { useCallback } from 'react'; +import { Operator } from '../constant'; +import OperatorIcon from '../operator-icon'; +import useGraphStore from '../store'; + +export type DataflowTimelineProps = { + traceList?: ITraceData[]; +}; + +const END = 'END'; + +interface DataflowTrace { + datetime: string; + elapsed_time: number; + message: string; + progress: number; + timestamp: number; +} +export function DataflowTimeline({ traceList }: DataflowTimelineProps) { + const getNode = useGraphStore((state) => state.getNode); + + const getNodeData = useCallback( + (componentId: string) => { + return getNode(componentId)?.data; + }, + [getNode], + ); + + const getNodeLabel = useCallback( + (componentId: string) => { + return getNodeData(componentId)?.label as Operator; + }, + [getNodeData], + ); + + return ( + <Timeline> + {Array.isArray(traceList) && + traceList?.map((item, index) => { + const traces = item.trace as DataflowTrace[]; + const nodeLabel = getNodeLabel(item.component_id); + + const latest = traces[traces.length - 1]; + const progress = latest.progress * 100; + + return ( + <TimelineItem + key={item.component_id} + step={index} + className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8 pb-6" + > + <TimelineHeader> + <TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-7 bg-accent-primary" /> + <TimelineTitle className=""> + <TimelineContent + className={cn( + 'text-foreground rounded-lg border px-4 py-3', + )} + > + <section className="flex items-center justify-between mb-2"> + <span className="flex-1 truncate"> + {getNodeData(item.component_id)?.name || END} + </span> + <div className="flex-1 flex items-center gap-5"> + <Progress value={progress} className="h-1 flex-1" /> + <span className="text-accent-primary text-xs"> + {progress}% + </span> + </div> + </section> + <div className="divide-y space-y-1"> + {traces + .filter((x) => !isEmpty(x.message)) + .map((x, idx) => ( + <section + key={idx} + className="text-text-secondary text-xs space-x-2 py-2.5 !m-0" + > + <span>{x.datetime}</span> + {item.component_id !== 'END' && ( + <span + className={cn({ + 'text-state-error': + x.message.startsWith('[ERROR]'), + })} + > + {x.message} + </span> + )} + <span> + {x.elapsed_time.toString().slice(0, 6)}s + </span> + </section> + ))} + </div> + </TimelineContent> + </TimelineTitle> + <TimelineIndicator + className={cn( + 'border border-accent-primary group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-5 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7', + { + 'rounded bg-accent-primary': nodeLabel === Operator.Begin, + }, + )} + > + {item.component_id === END ? ( + <span className="rounded-full inline-block size-2 bg-accent-primary"></span> + ) : nodeLabel === Operator.Begin ? ( + <File className="size-3.5 text-bg-base"></File> + ) : ( + <OperatorIcon + name={nodeLabel} + className="size-3.5 rounded-full" + ></OperatorIcon> + )} + </TimelineIndicator> + </TimelineHeader> + </TimelineItem> + ); + })} + </Timeline> + ); +} diff --git a/web/src/pages/data-flow/log-sheet/index.tsx b/web/src/pages/data-flow/log-sheet/index.tsx new file mode 100644 index 000000000..f66bada1b --- /dev/null +++ b/web/src/pages/data-flow/log-sheet/index.tsx @@ -0,0 +1,111 @@ +import { SkeletonCard } from '@/components/skeleton-card'; +import { Button } from '@/components/ui/button'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { IModalProps } from '@/interfaces/common'; +import { cn } from '@/lib/utils'; +import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant'; +import { + ArrowUpRight, + CirclePause, + Logs, + SquareArrowOutUpRight, +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import 'react18-json-view/src/style.css'; +import { useParams } from 'umi'; +import { + isEndOutputEmpty, + useDownloadOutput, +} from '../hooks/use-download-output'; +import { UseFetchLogReturnType } from '../hooks/use-fetch-log'; +import { DataflowTimeline } from './dataflow-timeline'; + +type LogSheetProps = IModalProps<any> & { + handleCancel(): void; + uploadedFileData?: Record<string, any>; +} & Pick< + UseFetchLogReturnType, + 'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs' | 'messageId' + >; + +export function LogSheet({ + hideModal, + isParsing, + logs, + handleCancel, + isCompleted, + isLogEmpty, + messageId, + uploadedFileData, +}: LogSheetProps) { + const { t } = useTranslation(); + const { id } = useParams(); + const { data: agent } = useFetchAgent(); + + const { handleDownloadJson } = useDownloadOutput(logs); + const { navigateToDataflowResult } = useNavigatePage(); + + return ( + <Sheet open onOpenChange={hideModal} modal={false}> + <SheetContent + className={cn('top-20')} + onInteractOutside={(e) => e.preventDefault()} + > + <SheetHeader> + <SheetTitle className="flex items-center gap-2.5"> + <Logs className="size-4" /> {t('flow.log')} + <Button + variant={'ghost'} + disabled={!isCompleted} + onClick={navigateToDataflowResult({ + id: messageId, // 'log_id', + [PipelineResultSearchParams.AgentId]: id, // 'agent_id', + [PipelineResultSearchParams.DocumentId]: uploadedFileData?.id, //'doc_id', + [PipelineResultSearchParams.AgentTitle]: agent.title, //'title', + [PipelineResultSearchParams.IsReadOnly]: 'true', + [PipelineResultSearchParams.Type]: 'dataflow', + [PipelineResultSearchParams.CreatedBy]: + uploadedFileData?.created_by, + [PipelineResultSearchParams.DocumentExtension]: + uploadedFileData?.extension, + })} + > + {t('dataflow.viewResult')} <ArrowUpRight /> + </Button> + </SheetTitle> + </SheetHeader> + <section className="max-h-[82vh] overflow-auto mt-6"> + {isLogEmpty ? ( + <SkeletonCard className="mt-2" /> + ) : ( + <DataflowTimeline traceList={logs}></DataflowTimeline> + )} + </section> + {isParsing ? ( + <Button + className="w-full mt-8 bg-state-error/10 text-state-error hover:bg-state-error hover:text-bg-base" + onClick={handleCancel} + > + <CirclePause /> {t('dataflow.cancel')} + </Button> + ) : ( + <Button + onClick={handleDownloadJson} + disabled={isEndOutputEmpty(logs)} + className="w-full mt-8 bg-accent-primary-5 text-text-secondary hover:bg-accent-primary-5 hover:text-accent-primary hover:border-accent-primary hover:border" + > + <SquareArrowOutUpRight /> + {t('dataflow.exportJson')} + </Button> + )} + </SheetContent> + </Sheet> + ); +} diff --git a/web/src/pages/data-flow/operator-icon.tsx b/web/src/pages/data-flow/operator-icon.tsx index 2184569d6..187c84cf5 100644 --- a/web/src/pages/data-flow/operator-icon.tsx +++ b/web/src/pages/data-flow/operator-icon.tsx @@ -1,9 +1,11 @@ import { IconFont } from '@/components/icon-font'; import { cn } from '@/lib/utils'; import { + Blocks, + File, FileChartColumnIncreasing, - Grid3x3, - HousePlus, + FileStack, + Heading, ListMinus, } from 'lucide-react'; import { Operator } from './constant'; @@ -14,26 +16,16 @@ interface IProps { } 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.Begin]: File, [Operator.Parser]: FileChartColumnIncreasing, - [Operator.Chunker]: Grid3x3, [Operator.Tokenizer]: ListMinus, + [Operator.Splitter]: Blocks, + [Operator.HierarchicalMerger]: Heading, + [Operator.Extractor]: FileStack, }; const Empty = () => { @@ -52,7 +44,7 @@ const OperatorIcon = ({ name, className }: IProps) => { className, )} > - <HousePlus className="rounded size-3" /> + <File className="rounded size-3" /> </div> ); } @@ -60,7 +52,7 @@ const OperatorIcon = ({ name, className }: IProps) => { return typeof Icon === 'string' ? ( <IconFont name={Icon} className={cn('size-5 ', className)}></IconFont> ) : ( - <SvgIcon className="size-5"></SvgIcon> + <SvgIcon className={cn('size-5', className)}></SvgIcon> ); }; diff --git a/web/src/pages/data-flow/options.ts b/web/src/pages/data-flow/options.ts index f231ec450..e69de29bb 100644 --- a/web/src/pages/data-flow/options.ts +++ b/web/src/pages/data-flow/options.ts @@ -1,2178 +0,0 @@ -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', - 'postgres', - 'mariadb', - 'mssql', - 'IBM DB2', -].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 index cac62d008..7ee9adc35 100644 --- a/web/src/pages/data-flow/run-sheet/index.tsx +++ b/web/src/pages/data-flow/run-sheet/index.tsx @@ -6,60 +6,22 @@ import { } 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'; +import { RunDataflowType } from '../hooks/use-run-dataflow'; +import { UploaderForm } from './uploader'; -const RunSheet = ({ - hideModal, - showModal: showChatModal, -}: IModalProps<any>) => { +type RunSheetProps = IModalProps<any> & + Pick<RunDataflowType, 'run' | 'loading'>; + +const RunSheet = ({ hideModal, run, loading }: RunSheetProps) => { 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<string, BeginQuery> = 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 ( - <Sheet onOpenChange={hideModal} open> + <Sheet onOpenChange={hideModal} open modal={false}> <SheetContent className={cn('top-20 p-2')}> <SheetHeader> <SheetTitle>{t('flow.testRun')}</SheetTitle> - <DebugContent - ok={onOk} - parameters={inputs} - loading={loading} - ></DebugContent> + <UploaderForm ok={run} loading={loading}></UploaderForm> </SheetHeader> </SheetContent> </Sheet> diff --git a/web/src/pages/data-flow/run-sheet/uploader.tsx b/web/src/pages/data-flow/run-sheet/uploader.tsx new file mode 100644 index 000000000..db4a977dd --- /dev/null +++ b/web/src/pages/data-flow/run-sheet/uploader.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { z } from 'zod'; + +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { ButtonLoading } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { FileUploadDirectUpload } from '@/pages/agent/debug-content/uploader'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +const formSchema = z.object({ + file: z.record(z.any()), +}); + +export type FormSchemaType = z.infer<typeof formSchema>; + +type UploaderFormProps = { + ok: (values: FormSchemaType) => void; + loading: boolean; +}; + +export function UploaderForm({ ok, loading }: UploaderFormProps) { + const { t } = useTranslation(); + const form = useForm<FormSchemaType>({ + resolver: zodResolver(formSchema), + defaultValues: {}, + }); + + return ( + <Form {...form}> + <form onSubmit={form.handleSubmit(ok)} className="space-y-8"> + <RAGFlowFormItem name="file"> + {(field) => { + return ( + <FileUploadDirectUpload + value={field.value} + onChange={field.onChange} + ></FileUploadDirectUpload> + ); + }} + </RAGFlowFormItem> + + <div> + <ButtonLoading + type="submit" + loading={loading} + className="w-full mt-1" + > + {t('flow.run')} + </ButtonLoading> + </div> + </form> + </Form> + ); +} diff --git a/web/src/pages/data-flow/store.ts b/web/src/pages/data-flow/store.ts index e163ff8cb..64fda97a4 100644 --- a/web/src/pages/data-flow/store.ts +++ b/web/src/pages/data-flow/store.ts @@ -14,7 +14,6 @@ import { applyEdgeChanges, applyNodeChanges, } from '@xyflow/react'; -import { omit } from 'lodash'; import differenceWith from 'lodash/differenceWith'; import intersectionWith from 'lodash/intersectionWith'; import lodashSet from 'lodash/set'; @@ -59,7 +58,6 @@ export type RFState = { updateNode: (node: RAGFlowNodeType) => void; addEdge: (connection: Connection) => void; getEdge: (id: string) => Edge | undefined; - updateFormDataOnConnect: (connection: Connection) => void; updateSwitchFormData: ( source: string, sourceHandle?: string | null, @@ -67,7 +65,6 @@ export type RFState = { isConnecting?: boolean, ) => void; duplicateNode: (id: string, name: string) => void; - duplicateIterationNode: (id: string, name: string) => void; deleteEdge: () => void; deleteEdgeById: (id: string) => void; deleteNodeById: (id: string) => void; @@ -89,6 +86,7 @@ export type RFState = { ) => void; // Deleting a condition of a classification operator will delete the related edge findAgentToolNodeById: (id: string | null) => string | undefined; selectNodeIds: (nodeIds: string[]) => void; + hasChildNode: (nodeId: string) => boolean; }; // this is our useStore hook that we can use in our components to get parts of the store and call actions @@ -126,11 +124,9 @@ const useGraphStore = create<RFState>()( setEdges(mapEdgeMouseEvent(edges, edgeId, false)); }, onConnect: (connection: Connection) => { - const { updateFormDataOnConnect } = get(); set({ edges: addEdge(connection, get().edges), }); - updateFormDataOnConnect(connection); }, onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => { set({ @@ -217,37 +213,14 @@ const useGraphStore = create<RFState>()( set({ edges: addEdge(connection, get().edges), }); - // TODO: This may not be reasonable. You need to choose between listening to changes in the form. - get().updateFormDataOnConnect(connection); }, getEdge: (id: string) => { return get().edges.find((x) => x.id === id); }, - updateFormDataOnConnect: (connection: Connection) => { - const { getOperatorTypeFromId, updateSwitchFormData } = get(); - const { source, target, sourceHandle } = connection; - const operatorType = getOperatorTypeFromId(source); - if (source) { - switch (operatorType) { - case Operator.Switch: { - updateSwitchFormData(source, sourceHandle, target, true); - break; - } - default: - break; - } - } - }, duplicateNode: (id: string, name: string) => { - const { getNode, addNode, generateNodeName, duplicateIterationNode } = - get(); + const { getNode, addNode, generateNodeName } = get(); const node = getNode(id); - if (node?.data.label === Operator.Iteration) { - duplicateIterationNode(id, name); - return; - } - addNode({ ...(node || {}), data: { @@ -257,35 +230,6 @@ const useGraphStore = create<RFState>()( ...generateDuplicateNode(node?.position, node?.data?.label), }); }, - duplicateIterationNode: (id: string, name: string) => { - const { getNode, generateNodeName, nodes } = get(); - const node = getNode(id); - - const iterationNode: RAGFlowNodeType = { - ...(node || {}), - data: { - ...(node?.data || { label: Operator.Iteration, form: {} }), - name: generateNodeName(name), - }, - ...generateDuplicateNode(node?.position, node?.data?.label), - }; - - const children = nodes - .filter((x) => x.parentId === node?.id) - .map((x) => ({ - ...(x || {}), - data: { - ...duplicateNodeForm(x?.data), - name: generateNodeName(x.data.name), - }, - ...omit(generateDuplicateNode(x?.position, x?.data?.label), [ - 'position', - ]), - parentId: iterationNode.id, - })); - - set({ nodes: nodes.concat(iterationNode, ...children) }); - }, deleteEdge: () => { const { edges, selectedEdgeIds } = get(); set({ @@ -295,55 +239,15 @@ const useGraphStore = create<RFState>()( }); }, deleteEdgeById: (id: string) => { - const { - edges, - updateNodeForm, - getOperatorTypeFromId, - updateSwitchFormData, - } = get(); - const currentEdge = edges.find((x) => x.id === id); + const { edges } = get(); - if (currentEdge) { - const { source, sourceHandle, target } = currentEdge; - const operatorType = getOperatorTypeFromId(source); - // After deleting the edge, set the corresponding field in the node's form field to undefined - switch (operatorType) { - case Operator.Relevant: - updateNodeForm(source, { - [sourceHandle as string]: undefined, - }); - break; - // case Operator.Categorize: - // if (sourceHandle) - // updateNodeForm(source, undefined, [ - // 'category_description', - // sourceHandle, - // 'to', - // ]); - // break; - case Operator.Switch: { - updateSwitchFormData(source, sourceHandle, target, false); - break; - } - default: - break; - } - } set({ edges: edges.filter((edge) => edge.id !== id), }); }, deleteNodeById: (id: string) => { - const { - nodes, - edges, - getOperatorTypeFromId, - deleteAgentDownstreamNodesById, - } = get(); - if (getOperatorTypeFromId(id) === Operator.Agent) { - deleteAgentDownstreamNodesById(id); - return; - } + const { nodes, edges } = get(); + set({ nodes: nodes.filter((node) => node.id !== id), edges: edges @@ -526,6 +430,10 @@ const useGraphStore = create<RFState>()( })), ); }, + hasChildNode: (nodeId) => { + const { edges } = get(); + return edges.some((edge) => edge.source === nodeId); + }, })), { name: 'graph', trace: true }, ), diff --git a/web/src/pages/data-flow/upload-agent-dialog/index.tsx b/web/src/pages/data-flow/upload-agent-dialog/index.tsx deleted file mode 100644 index 45f9ec882..000000000 --- a/web/src/pages/data-flow/upload-agent-dialog/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { LoadingButton } from '@/components/ui/loading-button'; -import { IModalProps } from '@/interfaces/common'; -import { TagRenameId } from '@/pages/add-knowledge/constant'; -import { useTranslation } from 'react-i18next'; -import { UploadAgentForm } from './upload-agent-form'; - -export function UploadAgentDialog({ - hideModal, - onOk, - loading, -}: IModalProps<any>) { - const { t } = useTranslation(); - - return ( - <Dialog open onOpenChange={hideModal}> - <DialogContent className="sm:max-w-[425px]"> - <DialogHeader> - <DialogTitle>{t('fileManager.uploadFile')}</DialogTitle> - </DialogHeader> - <UploadAgentForm hideModal={hideModal} onOk={onOk}></UploadAgentForm> - <DialogFooter> - <LoadingButton type="submit" form={TagRenameId} loading={loading}> - {t('common.save')} - </LoadingButton> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/web/src/pages/data-flow/upload-agent-dialog/upload-agent-form.tsx b/web/src/pages/data-flow/upload-agent-dialog/upload-agent-form.tsx deleted file mode 100644 index 39a4ac012..000000000 --- a/web/src/pages/data-flow/upload-agent-dialog/upload-agent-form.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import { FileUploader } from '@/components/file-uploader'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { FileMimeType, Platform } from '@/constants/common'; -import { IModalProps } from '@/interfaces/common'; -import { TagRenameId } from '@/pages/add-knowledge/constant'; -import { useTranslation } from 'react-i18next'; - -// const options = Object.values(Platform).map((x) => ({ label: x, value: x })); - -export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) { - const { t } = useTranslation(); - const FormSchema = z.object({ - platform: z - .string() - .min(1, { - message: t('common.namePlaceholder'), - }) - .trim(), - fileList: z.array(z.instanceof(File)), - }); - - const form = useForm<z.infer<typeof FormSchema>>({ - resolver: zodResolver(FormSchema), - defaultValues: { platform: Platform.RAGFlow }, - }); - - async function onSubmit(data: z.infer<typeof FormSchema>) { - console.log('🚀 ~ onSubmit ~ data:', data); - const ret = await onOk?.(data); - if (ret) { - hideModal?.(); - } - } - - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - className="space-y-6" - id={TagRenameId} - > - <FormField - control={form.control} - name="fileList" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('common.name')}</FormLabel> - <FormControl> - <FileUploader - value={field.value} - onValueChange={field.onChange} - maxFileCount={1} - accept={{ '*.json': [FileMimeType.Json] }} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - {/* <FormField - control={form.control} - name="platform" - render={({ field }) => ( - <FormItem> - <FormLabel>{t('common.name')}</FormLabel> - <FormControl> - <RAGFlowSelect {...field} options={options} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> */} - </form> - </Form> - ); -} diff --git a/web/src/pages/data-flow/utils.ts b/web/src/pages/data-flow/utils.ts index c7d60dfd2..e766e0851 100644 --- a/web/src/pages/data-flow/utils.ts +++ b/web/src/pages/data-flow/utils.ts @@ -1,56 +1,36 @@ -import { - IAgentForm, - ICategorizeForm, - ICategorizeItem, - ICategorizeItemResult, -} from '@/interfaces/database/agent'; +import { IAgentForm } from '@/interfaces/database/agent'; import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow'; -import { removeUselessFieldsFromValues } from '@/utils/form'; -import { Edge, Node, XYPosition } from '@xyflow/react'; +import { Edge, XYPosition } from '@xyflow/react'; import { FormInstance, FormListFieldData } from 'antd'; import { humanId } from 'human-id'; -import { curry, get, intersectionWith, isEqual, omit, sample } from 'lodash'; +import { curry, get, intersectionWith, isEmpty, isEqual, sample } from 'lodash'; import pipe from 'lodash/fp/pipe'; import isObject from 'lodash/isObject'; import { CategorizeAnchorPointPositions, + FileType, + FileTypeSuffixMap, NoDebugOperatorsList, NodeHandleId, Operator, } from './constant'; -import { BeginQuery, IPosition } from './interface'; - -function buildAgentExceptionGoto(edges: Edge[], nodeId: string) { - const exceptionEdges = edges.filter( - (x) => - x.source === nodeId && x.sourceHandle === NodeHandleId.AgentException, - ); - - return exceptionEdges.map((x) => x.target); -} +import { ExtractorFormSchemaType } from './form/extractor-form'; +import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form'; +import { ParserFormSchemaType } from './form/parser-form'; +import { SplitterFormSchemaType } from './form/splitter-form'; +import { IPosition } from './interface'; const buildComponentDownstreamOrUpstream = ( edges: Edge[], nodeId: string, isBuildDownstream = true, - nodes: Node[], ) => { return edges .filter((y) => { - const node = nodes.find((x) => x.id === nodeId); let isNotUpstreamTool = true; let isNotUpstreamAgent = true; let isNotExceptionGoto = true; - if (isBuildDownstream && node?.data.label === Operator.Agent) { - isNotExceptionGoto = y.sourceHandle !== NodeHandleId.AgentException; - // Exclude the tool operator downstream of the agent operator - isNotUpstreamTool = !y.target.startsWith(Operator.Tool); - // Exclude the agent operator downstream of the agent operator - isNotUpstreamAgent = !( - y.target.startsWith(Operator.Agent) && - y.targetHandle === NodeHandleId.AgentTop - ); - } + return ( y[isBuildDownstream ? 'source' : 'target'] === nodeId && isNotUpstreamTool && @@ -63,79 +43,12 @@ const buildComponentDownstreamOrUpstream = ( const removeUselessDataInTheOperator = curry( (operatorName: string, params: Record<string, unknown>) => { - if ( - operatorName === Operator.Generate || - operatorName === Operator.Categorize - ) { - return removeUselessFieldsFromValues(params, ''); - } + // if (operatorName === Operator.Categorize) { + // return removeUselessFieldsFromValues(params, ''); + // } return params; }, ); -// initialize data for operators without parameters -// const initializeOperatorParams = curry((operatorName: string, values: any) => { -// if (isEmpty(values)) { -// return initialFormValuesMap[operatorName as Operator]; -// } -// return values; -// }); - -function buildAgentTools(edges: Edge[], nodes: Node[], nodeId: string) { - const node = nodes.find((x) => x.id === nodeId); - const params = { ...(node?.data.form ?? {}) }; - if (node && node.data.label === Operator.Agent) { - const bottomSubAgentEdges = edges.filter( - (x) => x.source === nodeId && x.sourceHandle === NodeHandleId.AgentBottom, - ); - - (params as IAgentForm).tools = (params as IAgentForm).tools.concat( - bottomSubAgentEdges.map((x) => { - const { - params: formData, - id, - name, - } = buildAgentTools(edges, nodes, x.target); - - return { - component_name: Operator.Agent, - id, - name: name as string, // Cast name to string and provide fallback - params: { ...formData }, - }; - }), - ); - } - return { params, name: node?.data.name, id: node?.id }; -} - -function filterTargetsBySourceHandleId(edges: Edge[], handleId: string) { - return edges.filter((x) => x.sourceHandle === handleId).map((x) => x.target); -} - -function buildCategorize(edges: Edge[], nodes: Node[], nodeId: string) { - const node = nodes.find((x) => x.id === nodeId); - const params = { ...(node?.data.form ?? {}) } as ICategorizeForm; - if (node && node.data.label === Operator.Categorize) { - const subEdges = edges.filter((x) => x.source === nodeId); - - const items = params.items || []; - - const nextCategoryDescription = items.reduce< - ICategorizeForm['category_description'] - >((pre, val) => { - const key = val.name; - pre[key] = { - ...omit(val, 'name', 'uuid'), - examples: val.examples?.map((x) => x.value) || [], - to: filterTargetsBySourceHandleId(subEdges, val.uuid), - }; - return pre; - }, {}); - - params.category_description = nextCategoryDescription; - } - return omit(params, 'items'); -} const buildOperatorParams = (operatorName: string) => pipe( @@ -143,7 +56,7 @@ const buildOperatorParams = (operatorName: string) => // initializeOperatorParams(operatorName), // Final processing, for guarantee ); -const ExcludeOperators = [Operator.Note, Operator.Tool]; +const ExcludeOperators = [Operator.Note]; export function isBottomSubAgent(edges: Edge[], nodeId?: string) { const edge = edges.find( @@ -151,6 +64,90 @@ export function isBottomSubAgent(edges: Edge[], nodeId?: string) { ); return !!edge; } +// Because the array of react-hook-form must be object data, +// it needs to be converted into a simple data type array required by the backend +function transformObjectArrayToPureArray( + list: Array<Record<string, any>>, + field: string, +) { + return Array.isArray(list) + ? list.filter((x) => !isEmpty(x[field])).map((y) => y[field]) + : []; +} + +function transformParserParams(params: ParserFormSchemaType) { + const setups = params.setups.reduce< + Record<string, ParserFormSchemaType['setups'][0]> + >((pre, cur) => { + if (cur.fileFormat) { + let filteredSetup: Partial< + ParserFormSchemaType['setups'][0] & { suffix: string[] } + > = { + output_format: cur.output_format, + suffix: FileTypeSuffixMap[cur.fileFormat as FileType], + }; + + switch (cur.fileFormat) { + case FileType.PDF: + filteredSetup = { + ...filteredSetup, + parse_method: cur.parse_method, + lang: cur.lang, + }; + break; + case FileType.Image: + filteredSetup = { + ...filteredSetup, + parse_method: cur.parse_method, + lang: cur.lang, + system_prompt: cur.system_prompt, + }; + break; + case FileType.Email: + filteredSetup = { + ...filteredSetup, + fields: cur.fields, + }; + break; + case FileType.Video: + case FileType.Audio: + filteredSetup = { + ...filteredSetup, + llm_id: cur.llm_id, + }; + break; + default: + break; + } + + pre[cur.fileFormat] = filteredSetup; + } + return pre; + }, {}); + + return { ...params, setups }; +} + +function transformSplitterParams(params: SplitterFormSchemaType) { + return { + ...params, + delimiters: transformObjectArrayToPureArray(params.delimiters, 'value'), + }; +} + +function transformHierarchicalMergerParams( + params: HierarchicalMergerFormSchemaType, +) { + const levels = params.levels.map((x) => + transformObjectArrayToPureArray(x.expressions, 'expression'), + ); + + return { ...params, hierarchy: Number(params.hierarchy), levels }; +} + +function transformExtractorParams(params: ExtractorFormSchemaType) { + return { ...params, prompts: [{ content: params.prompts, role: 'user' }] }; +} // construct a dsl based on the node information of the graph export const buildDslComponentsByGraph = ( @@ -172,16 +169,19 @@ export const buildDslComponentsByGraph = ( let params = x?.data.form ?? {}; switch (operatorName) { - case Operator.Agent: { - const { params: formData } = buildAgentTools(edges, nodes, id); - params = { - ...formData, - exception_goto: buildAgentExceptionGoto(edges, id), - }; + case Operator.Parser: + params = transformParserParams(params); break; - } - case Operator.Categorize: - params = buildCategorize(edges, nodes, id); + + case Operator.Splitter: + params = transformSplitterParams(params); + break; + + case Operator.HierarchicalMerger: + params = transformHierarchicalMergerParams(params); + break; + case Operator.Extractor: + params = transformExtractorParams(params); break; default: @@ -194,8 +194,8 @@ export const buildDslComponentsByGraph = ( component_name: operatorName, params: buildOperatorParams(operatorName)(params) ?? {}, }, - downstream: buildComponentDownstreamOrUpstream(edges, id, true, nodes), - upstream: buildComponentDownstreamOrUpstream(edges, id, false, nodes), + downstream: buildComponentDownstreamOrUpstream(edges, id, true), + upstream: buildComponentDownstreamOrUpstream(edges, id, false), parent_id: x?.parentId, }; }); @@ -294,10 +294,6 @@ export const getOtherFieldValues = ( x !== form.getFieldValue([formListName, field.name, latestField]), ); -export const generateSwitchHandleText = (idx: number) => { - return `Case ${idx + 1}`; -}; - export const getNodeDragHandle = (nodeType?: string) => { return nodeType === Operator.Note ? '.note-drag-handle' : undefined; }; @@ -353,25 +349,6 @@ export const generateNodeNamesWithIncreasingIndex = ( export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => { const form: Record<string, any> = { ...(nodeData?.form ?? {}) }; - // Delete the downstream node corresponding to the to field of the Categorize operator - if (nodeData?.label === Operator.Categorize) { - form.category_description = Object.keys(form.category_description).reduce< - Record<string, Record<string, any>> - >((pre, cur) => { - pre[cur] = { - ...form.category_description[cur], - to: undefined, - }; - return pre; - }, {}); - } - - // Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator - if (nodeData?.label === Operator.Relevant) { - form.yes = undefined; - form.no = undefined; - } - return { ...(nodeData ?? { label: '' }), form, @@ -386,40 +363,6 @@ export const needsSingleStepDebugging = (label: string) => { return !NoDebugOperatorsList.some((x) => (label as Operator) === x); }; -// Get the coordinates of the node relative to the Iteration node -export function getRelativePositionToIterationNode( - nodes: RAGFlowNodeType[], - position?: XYPosition, // relative position -) { - if (!position) { - return; - } - - const iterationNodes = nodes.filter( - (node) => node.data.label === Operator.Iteration, - ); - - for (const iterationNode of iterationNodes) { - const { - position: { x, y }, - width, - height, - } = iterationNode; - const halfWidth = (width || 0) / 2; - if ( - position.x >= x - halfWidth && - position.x <= x + halfWidth && - position.y >= y && - position.y <= y + (height || 0) - ) { - return { - parentId: iterationNode.id, - position: { x: position.x - x + halfWidth, y: position.y - y }, - }; - } - } -} - export const generateDuplicateNode = ( position?: XYPosition, label?: string, @@ -454,71 +397,11 @@ export function convertToObjectArray(list: Array<string | number | boolean>) { return list.map((x) => ({ value: x })); } -/** - * convert the following object into a list - * - * { - "product_related": { - "description": "The question is about product usage, appearance and how it works.", - "examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?", - "to": "generate:0" - } - } -*/ -export const buildCategorizeListFromObject = ( - categorizeItem: ICategorizeItemResult, -) => { - // Categorize's to field has two data sources, with edges as the data source. - // Changes in the edge or to field need to be synchronized to the form field. - return Object.keys(categorizeItem) - .reduce<Array<ICategorizeItem>>((pre, cur) => { - // synchronize edge data to the to field - - pre.push({ - name: cur, - ...categorizeItem[cur], - examples: convertToObjectArray(categorizeItem[cur].examples), - }); - return pre; - }, []) - .sort((a, b) => a.index - b.index); -}; - -/** - * Convert the list in the following form into an object - * { - "items": [ - { - "name": "Categorize 1", - "description": "111", - "examples": ["ddd"], - "to": "Retrieval:LazyEelsStick" - } - ] - } -*/ -export const buildCategorizeObjectFromList = (list: Array<ICategorizeItem>) => { - return list.reduce<ICategorizeItemResult>((pre, cur) => { - if (cur?.name) { - pre[cur.name] = { - ...omit(cur, 'name', 'examples'), - examples: convertToStringArray(cur.examples) as string[], - }; - } - return pre; - }, {}); -}; - export function getAgentNodeTools(agentNode?: RAGFlowNodeType) { const tools: IAgentForm['tools'] = get(agentNode, 'data.form.tools', []); return tools; } -export function getAgentNodeMCP(agentNode?: RAGFlowNodeType) { - const tools: IAgentForm['mcp'] = get(agentNode, 'data.form.mcp', []); - return tools; -} - export function mapEdgeMouseEvent( edges: Edge[], edgeId: string, @@ -538,21 +421,3 @@ export function mapEdgeMouseEvent( return nextEdges; } - -export function buildBeginQueryWithObject( - inputs: Record<string, BeginQuery>, - values: BeginQuery[], -) { - const nextInputs = Object.keys(inputs).reduce<Record<string, BeginQuery>>( - (pre, key) => { - const item = values.find((x) => x.key === key); - if (item) { - pre[key] = { ...item }; - } - return pre; - }, - {}, - ); - - return nextInputs; -} diff --git a/web/src/pages/data-flow/utils/chat.ts b/web/src/pages/data-flow/utils/chat.ts deleted file mode 100644 index 15c8ff850..000000000 --- a/web/src/pages/data-flow/utils/chat.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MessageType } from '@/constants/chat'; -import { IReference } from '@/interfaces/database/chat'; -import { IMessage } from '@/pages/chat/interface'; -import { isEmpty } from 'lodash'; - -export const buildAgentMessageItemReference = ( - conversation: { message: IMessage[]; reference: IReference[] }, - message: IMessage, -) => { - const assistantMessages = conversation.message?.filter( - (x) => x.role === MessageType.Assistant, - ); - const referenceIndex = assistantMessages.findIndex( - (x) => x.id === message.id, - ); - const reference = !isEmpty(message?.reference) - ? message?.reference - : (conversation?.reference ?? [])[referenceIndex]; - - return reference ?? { doc_aggs: [], chunks: [], total: 0 }; -}; diff --git a/web/src/pages/dataflow-result/chunker.tsx b/web/src/pages/dataflow-result/chunker.tsx index ff869809d..1c194f76a 100644 --- a/web/src/pages/dataflow-result/chunker.tsx +++ b/web/src/pages/dataflow-result/chunker.tsx @@ -1,3 +1,4 @@ +import { TimelineNode } from '@/components/originui/timeline'; import message from '@/components/ui/message'; import { RAGFlowPagination, @@ -23,9 +24,16 @@ import { useUpdateChunk, } from './hooks'; import styles from './index.less'; -const ChunkerContainer = () => { + +interface IProps { + isChange: boolean; + setIsChange: (isChange: boolean) => void; + step?: TimelineNode; +} +const ChunkerContainer = (props: IProps) => { + const { isChange, setIsChange, step } = props; const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); - const [isChange, setIsChange] = useState(false); + const { t } = useTranslation(); const { data: { documentInfo, data = [], total }, @@ -135,19 +143,18 @@ const ChunkerContainer = () => { setIsChange(true); onChunkUpdatingOk(e); }; + + const handleReRunFunc = () => { + setIsChange(false); + }; return ( - <> + <div className="w-full h-full"> {isChange && ( <div className=" absolute top-2 right-6"> - <RerunButton /> + <RerunButton step={step} onRerun={handleReRunFunc} /> </div> )} - <div - className={classNames( - { [styles.pagePdfWrapper]: isPdf }, - 'flex flex-col w-3/5', - )} - > + <div className={classNames('flex flex-col w-full')}> <Spin spinning={loading} className={styles.spin} size="large"> <div className="h-[50px] flex flex-row justify-between items-end pb-[5px]"> <div> @@ -176,7 +183,7 @@ const ChunkerContainer = () => { selectedChunkIds={selectedChunkIds} /> </div> - <div className="h-[calc(100vh-280px)] overflow-y-auto pr-2 scrollbar-thin"> + <div className="h-[calc(100vh-280px)] overflow-y-auto pr-2 scrollbar-auto"> <div className={classNames( styles.chunkContainer, @@ -227,7 +234,7 @@ const ChunkerContainer = () => { parserId={documentInfo.parser_id} /> )} - </> + </div> ); }; diff --git a/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx b/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx index 68bafeeb7..bf98299a2 100644 --- a/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx +++ b/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx @@ -1,24 +1,17 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; -import { Ban, CircleCheck, Trash2 } from 'lucide-react'; +import { Trash2 } from 'lucide-react'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; type ICheckboxSetProps = { selectAllChunk: (e: any) => void; removeChunk: (e?: any) => void; - switchChunk: (available: number) => void; checked: boolean; selectedChunkIds: string[]; }; export default (props: ICheckboxSetProps) => { - const { - selectAllChunk, - removeChunk, - switchChunk, - checked, - selectedChunkIds, - } = props; + const { selectAllChunk, removeChunk, checked, selectedChunkIds } = props; const { t } = useTranslation(); const handleSelectAllCheck = useCallback( (e: any) => { @@ -32,14 +25,6 @@ export default (props: ICheckboxSetProps) => { removeChunk(); }, [removeChunk]); - const handleEnabledClick = useCallback(() => { - switchChunk(1); - }, [switchChunk]); - - const handleDisabledClick = useCallback(() => { - switchChunk(0); - }, [switchChunk]); - const isSelected = useMemo(() => { return selectedChunkIds?.length > 0; }, [selectedChunkIds]); @@ -57,20 +42,6 @@ export default (props: ICheckboxSetProps) => { </div> {isSelected && ( <> - <div - className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary" - onClick={handleEnabledClick} - > - <CircleCheck size={16} /> - <span className="block ml-1">{t('chunk.enable')}</span> - </div> - <div - className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary" - onClick={handleDisabledClick} - > - <Ban size={16} /> - <span className="block ml-1">{t('chunk.disable')}</span> - </div> <div className="flex items-center cursor-pointer text-red-400 hover:text-red-500" onClick={handleDeleteClick} diff --git a/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx b/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx index 368317441..875e51596 100644 --- a/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx +++ b/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx @@ -1,55 +1,23 @@ -import { Input } from '@/components/originui/input'; import { Button } from '@/components/ui/button'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { Radio } from '@/components/ui/radio'; import { useTranslate } from '@/hooks/common-hooks'; import { cn } from '@/lib/utils'; -import { SearchOutlined } from '@ant-design/icons'; -import { ListFilter, Plus } from 'lucide-react'; +import { Plus } from 'lucide-react'; import { useState } from 'react'; import { ChunkTextMode } from '../../constant'; interface ChunkResultBarProps { changeChunkTextMode: React.Dispatch<React.SetStateAction<string | number>>; - available: number | undefined; - selectAllChunk: (value: boolean) => void; - handleSetAvailable: (value: number | undefined) => void; - createChunk: () => void; - handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void; - searchString: string; + createChunk: (text: string) => void; + isReadonly: boolean; } export default ({ changeChunkTextMode, - available, - selectAllChunk, - handleSetAvailable, createChunk, - handleInputChange, - searchString, + isReadonly, }: ChunkResultBarProps) => { const { t } = useTranslate('chunk'); const [textSelectValue, setTextSelectValue] = useState<string | number>( ChunkTextMode.Full, ); - const handleFilterChange = (e: string | number) => { - const value = e === -1 ? undefined : (e as number); - selectAllChunk(false); - handleSetAvailable(value); - }; - const filterContent = ( - <div className="w-[200px]"> - <Radio.Group onChange={handleFilterChange} value={available}> - <div className="flex flex-col gap-2 p-4"> - <Radio value={-1}>{t('all')}</Radio> - <Radio value={1}>{t('enabled')}</Radio> - <Radio value={0}>{t('disabled')}</Radio> - </div> - </Radio.Group> - </div> - ); const textSelectOptions = [ { label: t(ChunkTextMode.Full), value: ChunkTextMode.Full }, { label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse }, @@ -78,31 +46,15 @@ export default ({ </div> ))} </div> - <Input - className="bg-bg-card text-muted-foreground" - style={{ width: 200 }} - placeholder={t('search')} - icon={<SearchOutlined />} - onChange={handleInputChange} - value={searchString} - /> - <Popover> - <PopoverTrigger asChild> - <Button className="bg-bg-card text-muted-foreground hover:bg-card"> - <ListFilter /> - </Button> - </PopoverTrigger> - <PopoverContent className="p-0 w-[200px]"> - {filterContent} - </PopoverContent> - </Popover> - <Button - onClick={() => createChunk()} - variant={'secondary'} - className="bg-bg-card text-muted-foreground hover:bg-card" - > - <Plus size={44} /> - </Button> + {!isReadonly && ( + <Button + onClick={() => createChunk('')} + variant={'secondary'} + className="bg-bg-card text-muted-foreground hover:bg-card" + > + <Plus size={44} /> + </Button> + )} </div> ); }; diff --git a/web/src/pages/dataflow-result/components/document-preview/hooks.ts b/web/src/pages/dataflow-result/components/document-preview/hooks.ts index fcf6a01ba..30e5887d8 100644 --- a/web/src/pages/dataflow-result/components/document-preview/hooks.ts +++ b/web/src/pages/dataflow-result/components/document-preview/hooks.ts @@ -1,8 +1,9 @@ import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; -import { api_host } from '@/utils/api'; +import api, { api_host } from '@/utils/api'; import { useSize } from 'ahooks'; import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useGetPipelineResultSearchParams } from '../../hooks'; export const useDocumentResizeObserver = () => { const [containerWidth, setContainerWidth] = useState<number>(); @@ -44,12 +45,16 @@ export const useHighlightText = (searchText: string = '') => { return textRenderer; }; -export const useGetDocumentUrl = () => { +export const useGetDocumentUrl = (isAgent: boolean) => { const { documentId } = useGetKnowledgeSearchParams(); + const { createdBy, documentId: id } = useGetPipelineResultSearchParams(); const url = useMemo(() => { + if (isAgent) { + return api.downloadFile + `?id=${id}&created_by=${createdBy}`; + } return `${api_host}/document/get/${documentId}`; - }, [documentId]); + }, [createdBy, documentId, id, isAgent]); return url; }; diff --git a/web/src/pages/dataflow-result/components/parse-editer/hook.ts b/web/src/pages/dataflow-result/components/parse-editer/hook.ts new file mode 100644 index 000000000..6ec503acd --- /dev/null +++ b/web/src/pages/dataflow-result/components/parse-editer/hook.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef, useState } from 'react'; +import { IJsonContainerProps, IObjContainerProps } from './interface'; + +export const useParserInit = ({ + initialValue, +}: { + initialValue: + | IJsonContainerProps['initialValue'] + | IObjContainerProps['initialValue']; +}) => { + const [content, setContent] = useState(initialValue); + + useEffect(() => { + setContent(initialValue); + console.log('initialValue json parse', initialValue); + }, [initialValue]); + + const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>( + undefined, + ); + const editDivRef = useRef<HTMLDivElement>(null); + + return { + content, + setContent, + activeEditIndex, + setActiveEditIndex, + editDivRef, + }; +}; diff --git a/web/src/pages/dataflow-result/components/parse-editer/index.tsx b/web/src/pages/dataflow-result/components/parse-editer/index.tsx index 2a748e910..799cb11c9 100644 --- a/web/src/pages/dataflow-result/components/parse-editer/index.tsx +++ b/web/src/pages/dataflow-result/components/parse-editer/index.tsx @@ -1,45 +1,63 @@ -import { Textarea } from '@/components/ui/textarea'; -import { cn } from '@/lib/utils'; -import { useState } from 'react'; +import { CheckedState } from '@radix-ui/react-checkbox'; +import { FormatPreserveEditorProps } from './interface'; +import { ArrayContainer } from './json-parser'; +import { ObjectContainer } from './object-parser'; -interface FormatPreserveEditorProps { - initialValue: string; - onSave: (value: string) => void; - className?: string; -} const FormatPreserveEditor = ({ initialValue, onSave, className, + isChunck, + handleCheckboxClick, + selectedChunkIds, + textMode, + clickChunk, + isReadonly, }: FormatPreserveEditorProps) => { - const [content, setContent] = useState(initialValue); - const [isEditing, setIsEditing] = useState(false); + console.log('initialValue', initialValue); - const handleEdit = () => setIsEditing(true); - - const handleSave = () => { - onSave(content); - setIsEditing(false); + const escapeNewlines = (text: string) => { + return text.replace(/\n/g, '\\n'); + }; + const unescapeNewlines = (text: string) => { + return text.replace(/\\n/g, '\n'); + }; + const handleCheck = (e: CheckedState, id: string | number) => { + handleCheckboxClick?.(id, e === 'indeterminate' ? false : e); }; return ( <div className="editor-container"> - {isEditing ? ( - <Textarea - className={cn( - 'w-full h-full bg-transparent text-text-secondary', - className, - )} - value={content} - onChange={(e) => setContent(e.target.value)} - onBlur={handleSave} - autoSize={{ maxRows: 100 }} - autoFocus + {['json', 'chunks'].includes(initialValue.key) && ( + <ArrayContainer + isReadonly={isReadonly} + className={className} + initialValue={initialValue} + handleCheck={handleCheck} + selectedChunkIds={selectedChunkIds} + onSave={onSave} + escapeNewlines={escapeNewlines} + unescapeNewlines={unescapeNewlines} + textMode={textMode} + isChunck={isChunck} + clickChunk={clickChunk} + /> + )} + + {['text', 'html'].includes(initialValue.key) && ( + <ObjectContainer + isReadonly={isReadonly} + className={className} + initialValue={initialValue} + handleCheck={handleCheck} + selectedChunkIds={selectedChunkIds} + onSave={onSave} + escapeNewlines={escapeNewlines} + unescapeNewlines={unescapeNewlines} + textMode={textMode} + isChunck={isChunck} + clickChunk={clickChunk} /> - ) : ( - <pre className="text-text-secondary" onClick={handleEdit}> - {content} - </pre> )} </div> ); diff --git a/web/src/pages/dataflow-result/components/parse-editer/interface.ts b/web/src/pages/dataflow-result/components/parse-editer/interface.ts new file mode 100644 index 000000000..e45d20972 --- /dev/null +++ b/web/src/pages/dataflow-result/components/parse-editer/interface.ts @@ -0,0 +1,65 @@ +import { CheckedState } from '@radix-ui/react-checkbox'; +import { ChunkTextMode } from '../../constant'; +import { IChunk } from '../../interface'; +import { parserKeyMap } from './json-parser'; + +export interface FormatPreserveEditorProps { + initialValue: { + key: keyof typeof parserKeyMap | 'text' | 'html'; + type: string; + value: Array<{ [key: string]: string }>; + }; + onSave: (value: any) => void; + className?: string; + isSelect?: boolean; + isDelete?: boolean; + isChunck?: boolean; + handleCheckboxClick?: (id: string | number, checked: boolean) => void; + selectedChunkIds?: string[]; + textMode?: ChunkTextMode; + clickChunk: (chunk: IChunk) => void; + isReadonly: boolean; +} + +export type IJsonContainerProps = { + initialValue: { + key: keyof typeof parserKeyMap; + type: string; + value: { + [key: string]: string; + }[]; + }; + isChunck?: boolean; + handleCheck: (e: CheckedState, index: number) => void; + selectedChunkIds: string[] | undefined; + unescapeNewlines: (text: string) => string; + escapeNewlines: (text: string) => string; + onSave: (data: { + value: { + text: string; + }[]; + key: string; + type: string; + }) => void; + className?: string; + textMode?: ChunkTextMode; + clickChunk: (chunk: IChunk) => void; + isReadonly: boolean; +}; + +export type IObjContainerProps = { + initialValue: { + key: string; + type: string; + value: string; + }; + isChunck?: boolean; + handleCheck: (e: CheckedState, index: number) => void; + unescapeNewlines: (text: string) => string; + escapeNewlines: (text: string) => string; + onSave: (data: { value: string; key: string; type: string }) => void; + className?: string; + textMode?: ChunkTextMode; + clickChunk: (chunk: IChunk) => void; + isReadonly: boolean; +}; diff --git a/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx b/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx new file mode 100644 index 000000000..e86999b78 --- /dev/null +++ b/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx @@ -0,0 +1,139 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { cn } from '@/lib/utils'; +import { useCallback, useEffect } from 'react'; +import { ChunkTextMode } from '../../constant'; +import styles from '../../index.less'; +import { useParserInit } from './hook'; +import { IJsonContainerProps } from './interface'; +export const parserKeyMap = { + json: 'text', + chunks: 'text', +} as const; + +export const ArrayContainer = (props: IJsonContainerProps) => { + const { + initialValue, + isChunck, + handleCheck, + selectedChunkIds, + unescapeNewlines, + escapeNewlines, + onSave, + className, + textMode, + clickChunk, + isReadonly, + } = props; + + const { + content, + setContent, + activeEditIndex, + setActiveEditIndex, + editDivRef, + } = useParserInit({ initialValue }); + + const parserKey = parserKeyMap[content.key as keyof typeof parserKeyMap]; + + const handleEdit = useCallback( + (e?: any, index?: number) => { + setActiveEditIndex(index); + }, + [setContent, setActiveEditIndex], + ); + + const handleSave = useCallback( + (e: any) => { + const saveData = { + ...content, + value: content.value?.map((item, index) => { + if (index === activeEditIndex) { + return { + ...item, + [parserKey]: e.target.textContent || '', + }; + } else { + return item; + } + }), + }; + onSave(saveData); + setActiveEditIndex(undefined); + }, + [content, onSave], + ); + + useEffect(() => { + if (activeEditIndex !== undefined && editDivRef.current) { + editDivRef.current.focus(); + editDivRef.current.textContent = + content.value[activeEditIndex][parserKey]; + } + }, [editDivRef, activeEditIndex, content, parserKey]); + + return ( + <> + {content.value?.map((item, index) => { + if ( + item[parserKeyMap[content.key as keyof typeof parserKeyMap]] === '' + ) { + return null; + } + return ( + <section + key={index} + className={cn( + isChunck + ? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start' + : '', + activeEditIndex === index && isChunck ? 'bg-bg-title' : '', + )} + > + {isChunck && !isReadonly && ( + <Checkbox + onCheckedChange={(e) => { + handleCheck(e, index); + }} + checked={selectedChunkIds?.some( + (id) => id.toString() === index.toString(), + )} + ></Checkbox> + )} + {activeEditIndex === index && ( + <div + ref={editDivRef} + contentEditable={!isReadonly} + onBlur={handleSave} + className={cn( + 'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none p-0', + + className, + )} + ></div> + )} + {activeEditIndex !== index && ( + <div + className={cn( + 'text-text-secondary overflow-auto scrollbar-auto w-full', + { + [styles.contentEllipsis]: + textMode === ChunkTextMode.Ellipse, + }, + )} + key={index} + onClick={(e) => { + clickChunk(item); + if (!isReadonly) { + handleEdit(e, index); + } + }} + > + {item[parserKeyMap[content.key]]} + </div> + )} + </section> + ); + })} + </> + ); +}; diff --git a/web/src/pages/dataflow-result/components/parse-editer/object-parser.tsx b/web/src/pages/dataflow-result/components/parse-editer/object-parser.tsx new file mode 100644 index 000000000..bfb4b33f5 --- /dev/null +++ b/web/src/pages/dataflow-result/components/parse-editer/object-parser.tsx @@ -0,0 +1,96 @@ +import { cn } from '@/lib/utils'; +import { useCallback, useEffect } from 'react'; +import { ChunkTextMode } from '../../constant'; +import styles from '../../index.less'; +import { useParserInit } from './hook'; +import { IObjContainerProps } from './interface'; +export const ObjectContainer = (props: IObjContainerProps) => { + const { + initialValue, + isChunck, + unescapeNewlines, + escapeNewlines, + onSave, + className, + textMode, + clickChunk, + isReadonly, + } = props; + + const { + content, + setContent, + activeEditIndex, + setActiveEditIndex, + editDivRef, + } = useParserInit({ initialValue }); + + const handleEdit = useCallback(() => { + // setContent((pre) => ({ + // ...pre, + // value: escapeNewlines(e.target.innerText), + // })); + setActiveEditIndex(1); + }, [setContent, setActiveEditIndex]); + + const handleSave = useCallback( + (e: any) => { + const saveData = { + ...content, + value: e.target.textContent, + }; + onSave(saveData); + setActiveEditIndex(undefined); + }, + [content, onSave], + ); + + useEffect(() => { + if (activeEditIndex !== undefined && editDivRef.current) { + editDivRef.current.focus(); + editDivRef.current.textContent = content.value; + } + }, [activeEditIndex, content]); + + return ( + <> + <section + className={ + isChunck + ? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start' + : '' + } + > + {activeEditIndex && ( + <div + ref={editDivRef} + contentEditable={!isReadonly} + onBlur={handleSave} + className={cn( + 'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none p-0', + className, + )} + /> + )} + {!activeEditIndex && ( + <div + className={cn( + 'text-text-secondary overflow-auto scrollbar-auto whitespace-pre-wrap w-full', + { + [styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse, + }, + )} + onClick={(e) => { + clickChunk(content); + if (!isReadonly) { + handleEdit(e); + } + }} + > + {content.value} + </div> + )} + </section> + </> + ); +}; diff --git a/web/src/pages/dataflow-result/components/rerun-button/index.tsx b/web/src/pages/dataflow-result/components/rerun-button/index.tsx index 72edc3795..5b6015032 100644 --- a/web/src/pages/dataflow-result/components/rerun-button/index.tsx +++ b/web/src/pages/dataflow-result/components/rerun-button/index.tsx @@ -1,16 +1,44 @@ +import { TimelineNode } from '@/components/originui/timeline'; import SvgIcon from '@/components/svg-icon'; import { Button } from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal/modal'; import { CircleAlert } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useRerunDataflow } from '../../hooks'; interface RerunButtonProps { className?: string; + step?: TimelineNode; + onRerun?: () => void; + loading?: boolean; } const RerunButton = (props: RerunButtonProps) => { + const { className, step, onRerun, loading } = props; const { t } = useTranslation(); - const { loading } = useRerunDataflow(); const clickFunc = () => { console.log('click rerun button'); + Modal.show({ + visible: true, + className: '!w-[560px]', + title: t('dataflowParser.confirmRerun'), + children: ( + <div + dangerouslySetInnerHTML={{ + __html: t('dataflowParser.confirmRerunModalContent', { + step: step?.title, + }), + }} + ></div> + ), + okText: t('modal.okText'), + cancelText: t('modal.cancelText'), + onVisibleChange: (visible: boolean) => { + if (!visible) { + Modal.destroy(); + } else { + onRerun?.(); + Modal.destroy(); + } + }, + }); }; return ( <div className="flex flex-col gap-2"> diff --git a/web/src/pages/dataflow-result/components/time-line/index.tsx b/web/src/pages/dataflow-result/components/time-line/index.tsx index 75508a05f..92a1d236d 100644 --- a/web/src/pages/dataflow-result/components/time-line/index.tsx +++ b/web/src/pages/dataflow-result/components/time-line/index.tsx @@ -1,61 +1,74 @@ import { CustomTimeline, TimelineNode } from '@/components/originui/timeline'; import { - CheckLine, - FilePlayIcon, - Grid3x2, + Blocks, + File, + FilePlay, + FileStack, + Heading, ListPlus, - PlayIcon, } from 'lucide-react'; import { useMemo } from 'react'; +import { TimelineNodeType } from '../../constant'; +import { IPipelineFileLogDetail } from '../../interface'; + +export type ITimelineNodeObj = { + title: string; + icon: JSX.Element; + clickable?: boolean; + type: TimelineNodeType; +}; + export const TimelineNodeObj = { - begin: { - id: 1, - title: 'Begin', - icon: <PlayIcon size={13} />, + [TimelineNodeType.begin]: { + title: 'File', + icon: <File size={13} />, clickable: false, }, - parser: { id: 2, title: 'Parser', icon: <FilePlayIcon size={13} /> }, - chunker: { id: 3, title: 'Chunker', icon: <Grid3x2 size={13} /> }, - indexer: { - id: 4, - title: 'Indexer', + [TimelineNodeType.parser]: { + title: 'Parser', + icon: <FilePlay size={13} />, + }, + [TimelineNodeType.contextGenerator]: { + title: 'Context Generator', + icon: <FileStack size={13} />, + }, + [TimelineNodeType.titleSplitter]: { + title: 'Title Splitter', + icon: <Heading size={13} />, + }, + [TimelineNodeType.characterSplitter]: { + title: 'Character Splitter', + icon: <Blocks size={13} />, + }, + [TimelineNodeType.tokenizer]: { + title: 'Tokenizer', icon: <ListPlus size={13} />, clickable: false, }, - complete: { - id: 5, - title: 'Complete', - icon: <CheckLine size={13} />, - clickable: false, - }, }; - export interface TimelineDataFlowProps { activeId: number | string; - activeFunc: (id: number | string) => void; + activeFunc: (id: number | string, step: TimelineNode) => void; + data: IPipelineFileLogDetail; + timelineNodes: TimelineNode[]; } -const TimelineDataFlow = ({ activeFunc, activeId }: TimelineDataFlowProps) => { - // const [activeStep, setActiveStep] = useState(2); - const timelineNodes: TimelineNode[] = useMemo(() => { - const nodes: TimelineNode[] = []; - Object.keys(TimelineNodeObj).forEach((key) => { - nodes.push({ - ...TimelineNodeObj[key as keyof typeof TimelineNodeObj], - className: 'w-32', - completed: false, - }); - }); - return nodes; - }, []); +const TimelineDataFlow = ({ + activeFunc, + activeId, + data, + timelineNodes, +}: TimelineDataFlowProps) => { + // const [timelineNodeArr,setTimelineNodeArr] = useState<ITimelineNodeObj & {id: number | string}>() const activeStep = useMemo(() => { const index = timelineNodes.findIndex((node) => node.id === activeId); return index > -1 ? index + 1 : 0; }, [activeId, timelineNodes]); const handleStepChange = (step: number, id: string | number) => { - // setActiveStep(step); - activeFunc?.(id); - console.log(step, id); + activeFunc?.( + id, + timelineNodes.find((node) => node.id === activeStep) as TimelineNode, + ); }; return ( @@ -70,8 +83,8 @@ const TimelineDataFlow = ({ activeFunc, activeId }: TimelineDataFlowProps) => { nodeSize={24} activeStyle={{ nodeSize: 30, - iconColor: 'var(--accent-primary)', - textColor: 'var(--accent-primary)', + iconColor: 'rgb(var(--accent-primary))', + textColor: 'rgb(var(--accent-primary))', }} /> </div> diff --git a/web/src/pages/dataflow-result/constant.ts b/web/src/pages/dataflow-result/constant.ts index 2093acb8d..6d30ce122 100644 --- a/web/src/pages/dataflow-result/constant.ts +++ b/web/src/pages/dataflow-result/constant.ts @@ -2,3 +2,24 @@ export enum ChunkTextMode { Full = 'full', Ellipse = 'ellipse', } + +export enum TimelineNodeType { + begin = 'file', + parser = 'parser', + contextGenerator = 'extractor', + titleSplitter = 'hierarchicalMerger', + characterSplitter = 'splitter', + tokenizer = 'tokenizer', + end = 'end', +} + +export enum PipelineResultSearchParams { + DocumentId = 'doc_id', + KnowledgeId = 'knowledgeId', + Type = 'type', + IsReadOnly = 'is_read_only', + AgentId = 'agent_id', + AgentTitle = 'agent_title', + CreatedBy = 'created_by', // Who uploaded the file + DocumentExtension = 'extension', +} diff --git a/web/src/pages/dataflow-result/hooks.ts b/web/src/pages/dataflow-result/hooks.ts index be2dc1c01..8b984d8d8 100644 --- a/web/src/pages/dataflow-result/hooks.ts +++ b/web/src/pages/dataflow-result/hooks.ts @@ -1,42 +1,81 @@ +import { TimelineNode } from '@/components/originui/timeline'; import message from '@/components/ui/message'; -import { - useCreateChunk, - useDeleteChunk, - useSelectChunkList, -} from '@/hooks/chunk-hooks'; +import { useCreateChunk, useDeleteChunk } from '@/hooks/chunk-hooks'; import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; +import { useFetchMessageTrace } from '@/hooks/use-agent-request'; import { IChunk } from '@/interfaces/database/knowledge'; +import kbService from '@/services/knowledge-service'; +import { formatSecondsToHumanReadable } from '@/utils/date'; import { buildChunkHighlights } from '@/utils/document-util'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { t } from 'i18next'; +import { camelCase, upperFirst } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { IHighlight } from 'react-pdf-highlighter'; -import { ChunkTextMode } from './constant'; +import { useParams, useSearchParams } from 'umi'; +import { ITimelineNodeObj, TimelineNodeObj } from './components/time-line'; +import { + ChunkTextMode, + PipelineResultSearchParams, + TimelineNodeType, +} from './constant'; +import { IDslComponent, IPipelineFileLogDetail } from './interface'; + +export const useFetchPipelineFileLogDetail = ({ + isAgent = false, + isEdit = true, + refreshCount, +}: { + isEdit?: boolean; + refreshCount?: number; + isAgent: boolean; +}) => { + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const logId = searchParams.get('id') || id; + + let queryKey: (string | number)[] = []; + if (typeof refreshCount === 'number') { + queryKey = ['fetchLogDetail', refreshCount]; + } + + const { data, isFetching: loading } = useQuery<IPipelineFileLogDetail>({ + queryKey, + initialData: {} as IPipelineFileLogDetail, + gcTime: 0, + enabled: !isAgent, + queryFn: async () => { + if (isEdit) { + const { data } = await kbService.get_pipeline_detail({ + log_id: logId, + }); + return data?.data ?? {}; + } else { + return {}; + } + }, + }); + + return { data, loading }; +}; export const useHandleChunkCardClick = () => { - const [selectedChunkId, setSelectedChunkId] = useState<string>(''); + const [selectedChunk, setSelectedChunk] = useState<IChunk>(); - const handleChunkCardClick = useCallback((chunkId: string) => { - setSelectedChunkId(chunkId); + const handleChunkCardClick = useCallback((chunk: IChunk) => { + console.log('click-chunk-->', chunk); + setSelectedChunk(chunk); }, []); - return { handleChunkCardClick, selectedChunkId }; + return { handleChunkCardClick, selectedChunk }; }; -export const useGetSelectedChunk = (selectedChunkId: string) => { - const data = useSelectChunkList(); - return ( - data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk) - ); -}; - -export const useGetChunkHighlights = (selectedChunkId: string) => { +export const useGetChunkHighlights = (selectedChunk?: IChunk) => { const [size, setSize] = useState({ width: 849, height: 1200 }); - const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId); const highlights: IHighlight[] = useMemo(() => { - return buildChunkHighlights(selectedChunk, size); + return selectedChunk ? buildChunkHighlights(selectedChunk, size) : []; }, [selectedChunk, size]); const setWidthAndHeight = useCallback((width: number, height: number) => { @@ -131,55 +170,162 @@ export const useUpdateChunk = () => { }; }; -export const useFetchParserList = () => { - const [loading, setLoading] = useState(false); - return { - loading, - }; -}; +export const useRerunDataflow = ({ + data, +}: { + data: IPipelineFileLogDetail; +}) => { + const [isChange, setIsChange] = useState(false); -export const useRerunDataflow = () => { - const [loading, setLoading] = useState(false); - return { - loading, - }; -}; + const { mutateAsync: handleReRunFunc, isPending: loading } = useMutation({ + mutationKey: ['pipelineRerun', data], + mutationFn: async (newData: { value: IDslComponent; key: string }) => { + const newDsl = { + ...data.dsl, + components: { + ...data.dsl.components, + [newData.key]: newData.value, + }, + }; -export const useFetchPaserText = () => { - const initialText = - '第一行文本\n\t第二行缩进文本\n第三行 多个空格 第一行文本\n\t第二行缩进文本\n第三行 ' + - '多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + - '多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + - '多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + - '多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + - '多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + - '多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格'; - const [loading, setLoading] = useState(false); - const [data, setData] = useState<string>(initialText); - const { t } = useTranslation(); - const queryClient = useQueryClient(); - - const { - // data, - // isPending: loading, - mutateAsync, - } = useMutation({ - mutationKey: ['createChunk'], - mutationFn: async (payload: any) => { - // let service = kbService.create_chunk; - // if (payload.chunk_id) { - // service = kbService.set_chunk; - // } - // const { data } = await service(payload); - // if (data.code === 0) { - message.success(t('message.created')); - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] }); - }, 1000); // Delay to ensure the list is updated - // } - // return data?.code; + // this Data provided to the interface + const params = { + id: data.id, + dsl: newDsl, + component_id: newData.key, + }; + const { data: result } = await kbService.pipelineRerun(params); + if (result.code === 0) { + message.success(t('message.operated')); + // queryClient.invalidateQueries({ + // queryKey: [type], + // }); + } + return result; }, }); - return { data, loading, rerun: mutateAsync }; + return { + loading, + isChange, + setIsChange, + handleReRunFunc, + }; }; + +export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => { + const timelineNodes: TimelineNode[] = useMemo(() => { + const nodes: Array<ITimelineNodeObj & { id: number | string }> = []; + console.log('time-->', data); + const times = data?.dsl?.components; + if (times) { + const getNode = ( + key: string, + index: number, + type: + | TimelineNodeType.begin + | TimelineNodeType.parser + | TimelineNodeType.tokenizer + | TimelineNodeType.characterSplitter + | TimelineNodeType.titleSplitter, + ) => { + const node = times[key].obj; + const name = camelCase( + node.component_name, + ) as keyof typeof TimelineNodeObj; + + let tempType = type; + if (name === TimelineNodeType.parser) { + tempType = TimelineNodeType.parser; + } else if (name === TimelineNodeType.tokenizer) { + tempType = TimelineNodeType.tokenizer; + } else if ( + name === TimelineNodeType.characterSplitter || + name === TimelineNodeType.titleSplitter + ) { + tempType = TimelineNodeType.characterSplitter; + } + const timeNode = { + ...TimelineNodeObj[name], + id: index, + className: 'w-32', + completed: false, + date: formatSecondsToHumanReadable( + node.params?.outputs?._elapsed_time?.value || 0, + ), + type: tempType, + detail: { value: times[key], key: key }, + }; + console.log('timeNodetype-->', type); + nodes.push(timeNode); + + if (times[key].downstream && times[key].downstream.length > 0) { + const nextKey = times[key].downstream[0]; + + // nodes.push(timeNode); + getNode(nextKey, index + 1, tempType); + } + }; + getNode(upperFirst(TimelineNodeType.begin), 1, TimelineNodeType.begin); + // setTimelineNodeArr(nodes as unknown as ITimelineNodeObj & {id: number | string}) + } + return nodes; + }, [data]); + return { + timelineNodes, + }; +}; + +export const useGetPipelineResultSearchParams = () => { + const [currentQueryParameters] = useSearchParams(); + const is_read_only = currentQueryParameters.get( + PipelineResultSearchParams.IsReadOnly, + ) as 'true' | 'false'; + console.log('is_read_only', is_read_only); + return { + type: currentQueryParameters.get(PipelineResultSearchParams.Type) || '', + documentId: + currentQueryParameters.get(PipelineResultSearchParams.DocumentId) || '', + knowledgeId: + currentQueryParameters.get(PipelineResultSearchParams.KnowledgeId) || '', + isReadOnly: is_read_only === 'true', + agentId: + currentQueryParameters.get(PipelineResultSearchParams.AgentId) || '', + agentTitle: + currentQueryParameters.get(PipelineResultSearchParams.AgentTitle) || '', + documentExtension: + currentQueryParameters.get( + PipelineResultSearchParams.DocumentExtension, + ) || '', + createdBy: + currentQueryParameters.get(PipelineResultSearchParams.CreatedBy) || '', + }; +}; + +export function useFetchPipelineResult({ + agentId, +}: Pick<ReturnType<typeof useGetPipelineResultSearchParams>, 'agentId'>) { + const [searchParams] = useSearchParams(); + const messageId = searchParams.get('id'); + + const { data, setMessageId, setISStopFetchTrace } = + useFetchMessageTrace(agentId); + + useEffect(() => { + if (messageId) { + setMessageId(messageId); + setISStopFetchTrace(true); + } + }, [agentId, messageId, setISStopFetchTrace, setMessageId]); + + const pipelineResult = useMemo(() => { + if (Array.isArray(data)) { + const latest = data?.at(-1); + if (latest?.component_id === 'END' && Array.isArray(latest.trace)) { + return latest.trace.at(0); + } + } + }, [data]); + + return { pipelineResult }; +} diff --git a/web/src/pages/dataflow-result/index.less b/web/src/pages/dataflow-result/index.less index 9850fb410..e4a6574b1 100644 --- a/web/src/pages/dataflow-result/index.less +++ b/web/src/pages/dataflow-result/index.less @@ -82,15 +82,6 @@ } } -.card { - :global { - .ant-card-body { - padding: 10px; - margin: 0; - } - - margin-bottom: 10px; - } - - cursor: pointer; +.contentEllipsis { + .multipleLineEllipsis(3); } diff --git a/web/src/pages/dataflow-result/index.tsx b/web/src/pages/dataflow-result/index.tsx index f607f9266..9da4a910e 100644 --- a/web/src/pages/dataflow-result/index.tsx +++ b/web/src/pages/dataflow-result/index.tsx @@ -2,11 +2,21 @@ import { useFetchNextChunkList } from '@/hooks/use-chunk-request'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import DocumentPreview from './components/document-preview'; -import { useGetChunkHighlights, useHandleChunkCardClick } from './hooks'; +import { + useFetchPipelineFileLogDetail, + useFetchPipelineResult, + useGetChunkHighlights, + useGetPipelineResultSearchParams, + useHandleChunkCardClick, + useRerunDataflow, + useTimelineDataFlow, +} from './hooks'; import DocumentHeader from './components/document-preview/document-header'; +import { TimelineNode } from '@/components/originui/timeline'; import { PageHeader } from '@/components/page-header'; +import Spotlight from '@/components/spotlight'; import { Breadcrumb, BreadcrumbItem, @@ -15,35 +25,58 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; -import { - QueryStringMap, - useNavigatePage, -} from '@/hooks/logic-hooks/navigate-hooks'; -import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; -import { ChunkerContainer } from './chunker'; +import { Button } from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal/modal'; +import { Images } from '@/constants/common'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; import { useGetDocumentUrl } from './components/document-preview/hooks'; -import TimelineDataFlow, { TimelineNodeObj } from './components/time-line'; +import TimelineDataFlow from './components/time-line'; +import { TimelineNodeType } from './constant'; import styles from './index.less'; +import { IDslComponent, IPipelineFileLogDetail } from './interface'; import ParserContainer from './parser'; const Chunk = () => { + const { isReadOnly, knowledgeId, agentId, agentTitle, documentExtension } = + useGetPipelineResultSearchParams(); + + const isAgent = !!agentId; + + const { pipelineResult } = useFetchPipelineResult({ agentId }); + const { data: { documentInfo }, - } = useFetchNextChunkList(); - const { selectedChunkId } = useHandleChunkCardClick(); - const [activeStepId, setActiveStepId] = useState<number | string>(0); - const { data: dataset } = useFetchKnowledgeBaseConfiguration(); + } = useFetchNextChunkList(!isAgent); + const { selectedChunk, handleChunkCardClick } = useHandleChunkCardClick(); + const [activeStepId, setActiveStepId] = useState<number | string>(2); + const { data: dataset } = useFetchPipelineFileLogDetail({ + isAgent, + }); const { t } = useTranslation(); - const { navigateToDataset, getQueryString, navigateToDatasetList } = - useNavigatePage(); - const fileUrl = useGetDocumentUrl(); + const { timelineNodes } = useTimelineDataFlow( + agentId ? (pipelineResult as IPipelineFileLogDetail) : dataset, + ); + + const { + navigateToDataset, + navigateToDatasetList, + navigateToAgents, + navigateToDataflow, + } = useNavigatePage(); + let fileUrl = useGetDocumentUrl(isAgent); const { highlights, setWidthAndHeight } = - useGetChunkHighlights(selectedChunkId); + useGetChunkHighlights(selectedChunk); const fileType = useMemo(() => { + if (isAgent) { + return Images.some((x) => x === documentExtension) + ? 'visual' + : documentExtension; + } switch (documentInfo?.type) { case 'doc': return documentInfo?.name.split('.').pop() || 'doc'; @@ -55,29 +88,101 @@ const Chunk = () => { return documentInfo?.type; } return 'unknown'; - }, [documentInfo]); + }, [documentExtension, documentInfo?.name, documentInfo?.type, isAgent]); - const handleStepChange = (id: number | string) => { - setActiveStepId(id); + const { + handleReRunFunc, + isChange, + setIsChange, + loading: reRunLoading, + } = useRerunDataflow({ + data: dataset, + }); + + const handleStepChange = (id: number | string, step: TimelineNode) => { + if (isChange) { + Modal.show({ + visible: true, + className: '!w-[560px]', + title: t('dataflowParser.changeStepModalTitle'), + children: ( + <div + className="text-sm text-text-secondary" + dangerouslySetInnerHTML={{ + __html: t('dataflowParser.changeStepModalContent', { + step: step?.title, + }), + }} + ></div> + ), + onVisibleChange: () => { + Modal.destroy(); + }, + footer: ( + <div className="flex justify-end gap-2"> + <Button variant={'outline'} onClick={() => Modal.destroy()}> + {t('dataflowParser.changeStepModalCancelText')} + </Button> + <Button + variant={'secondary'} + className="!bg-state-error text-text-primary" + onClick={() => { + Modal.destroy(); + setActiveStepId(id); + setIsChange(false); + }} + > + {t('dataflowParser.changeStepModalConfirmText')} + </Button> + </div> + ), + }); + } else { + setActiveStepId(id); + } }; + + const { type } = useGetKnowledgeSearchParams(); + + const currentTimeNode: TimelineNode = useMemo(() => { + return ( + timelineNodes.find((node) => node.id === activeStepId) || + ({} as TimelineNode) + ); + }, [activeStepId, timelineNodes]); + return ( <> <PageHeader> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem> - <BreadcrumbLink onClick={navigateToDatasetList}> - {t('knowledgeDetails.dataset')} + <BreadcrumbLink + onClick={() => { + if (knowledgeId) { + navigateToDatasetList(); + } + if (agentId) { + navigateToAgents(); + } + }} + > + {knowledgeId ? t('knowledgeDetails.dataset') : t('header.flow')} </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> <BreadcrumbItem> <BreadcrumbLink - onClick={navigateToDataset( - getQueryString(QueryStringMap.id) as string, - )} + onClick={() => { + if (knowledgeId) { + navigateToDataset(knowledgeId)(); + } + if (agentId) { + navigateToDataflow(agentId)(); + } + }} > - {dataset.name} + {knowledgeId ? t('knowledgeDetails.overview') : agentTitle} </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> @@ -87,12 +192,16 @@ const Chunk = () => { </BreadcrumbList> </Breadcrumb> </PageHeader> - <div className=" absolute ml-[50%] translate-x-[-50%] top-4 flex justify-center"> - <TimelineDataFlow - activeFunc={handleStepChange} - activeId={activeStepId} - /> - </div> + {type === 'dataflow' && ( + <div className=" absolute ml-[50%] translate-x-[-50%] top-4 flex justify-center"> + <TimelineDataFlow + activeFunc={handleStepChange} + activeId={activeStepId} + data={dataset} + timelineNodes={timelineNodes} + /> + </div> + )} <div className={styles.chunkPage}> <div className="flex flex-none gap-8 border border-border mt-[26px] p-3 rounded-lg h-[calc(100vh-100px)]"> <div className="w-2/5"> @@ -110,8 +219,38 @@ const Chunk = () => { </section> </div> <div className="h-dvh border-r -mt-3"></div> - {activeStepId === TimelineNodeObj.chunker.id && <ChunkerContainer />} - {activeStepId === TimelineNodeObj.parser.id && <ParserContainer />} + <div className="w-3/5 h-full"> + {/* {currentTimeNode?.type === TimelineNodeType.splitter && ( + <ChunkerContainer + isChange={isChange} + setIsChange={setIsChange} + step={currentTimeNode as TimelineNode} + /> + )} */} + {/* {currentTimeNode?.type === TimelineNodeType.parser && ( */} + {(currentTimeNode?.type === TimelineNodeType.parser || + currentTimeNode?.type === TimelineNodeType.characterSplitter || + currentTimeNode?.type === TimelineNodeType.titleSplitter || + currentTimeNode?.type === TimelineNodeType.contextGenerator) && ( + <ParserContainer + isReadonly={isReadOnly} + isChange={isChange} + reRunLoading={reRunLoading} + setIsChange={setIsChange} + step={currentTimeNode as TimelineNode} + data={ + currentTimeNode.detail as { + value: IDslComponent; + key: string; + } + } + clickChunk={handleChunkCardClick} + reRunFunc={handleReRunFunc} + /> + )} + {/* )} */} + <Spotlight opcity={0.6} coverage={60} /> + </div> </div> </div> </> diff --git a/web/src/pages/dataflow-result/interface.ts b/web/src/pages/dataflow-result/interface.ts new file mode 100644 index 000000000..865044ce2 --- /dev/null +++ b/web/src/pages/dataflow-result/interface.ts @@ -0,0 +1,82 @@ +import { PipelineResultSearchParams } from './constant'; + +interface ComponentParams { + debug_inputs: Record<string, any>; + delay_after_error: number; + description: string; + exception_default_value: any; + exception_goto: any; + exception_method: any; + inputs: Record<string, any>; + max_retries: number; + message_history_window_size: number; + outputs: { + _created_time: Record<string, any>; + _elapsed_time: Record<string, any>; + name: Record<string, any>; + output_format: { type: string; value: string }; + json: { type: string; value: string }; + }; + persist_logs: boolean; + timeout: number; +} + +interface ComponentObject { + component_name: string; + params: ComponentParams; +} +export interface IDslComponent { + downstream: Array<string>; + obj: ComponentObject; + upstream: Array<string>; +} +export interface IPipelineFileLogDetail { + avatar: string; + create_date: string; + create_time: number; + document_id: string; + document_name: string; + document_suffix: string; + document_type: string; + dsl: { + components: { + [key: string]: IDslComponent; + }; + task_id: string; + path: Array<string>; + }; + id: string; + kb_id: string; + operation_status: string; + parser_id: string; + pipeline_id: string; + pipeline_title: string; + process_begin_at: string; + process_duration: number; + progress: number; + progress_msg: string; + source_from: string; + status: string; + task_type: string; + tenant_id: string; + update_date: string; + update_time: number; +} + +export interface IChunk { + positions: number[][]; + image_id: string; + text: string; +} + +export interface NavigateToDataflowResultProps { + id: string; + [PipelineResultSearchParams.KnowledgeId]?: string; + [PipelineResultSearchParams.DocumentId]: string; + [PipelineResultSearchParams.AgentId]?: string; + [PipelineResultSearchParams.AgentTitle]?: string; + [PipelineResultSearchParams.IsReadOnly]?: string; + [PipelineResultSearchParams.Type]: string; + [PipelineResultSearchParams.CreatedBy]: string; + [PipelineResultSearchParams.DocumentExtension]: string; +} diff --git a/web/src/pages/dataflow-result/parser.tsx b/web/src/pages/dataflow-result/parser.tsx index d6d278a95..41f4e2a39 100644 --- a/web/src/pages/dataflow-result/parser.tsx +++ b/web/src/pages/dataflow-result/parser.tsx @@ -1,38 +1,156 @@ +import { TimelineNode } from '@/components/originui/timeline'; import Spotlight from '@/components/spotlight'; -import { Spin } from '@/components/ui/spin'; +import { cn } from '@/lib/utils'; import classNames from 'classnames'; -import { useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import FormatPreserveEditor from './components/parse-editer'; +import ChunkResultBar from './components/chunk-result-bar'; +import CheckboxSets from './components/chunk-result-bar/checkbox-sets'; +import FormatPreserEditor from './components/parse-editer'; import RerunButton from './components/rerun-button'; -import { useFetchParserList, useFetchPaserText } from './hooks'; -const ParserContainer = () => { - const { data: initialValue, rerun: onSave } = useFetchPaserText(); +import { TimelineNodeType } from './constant'; +import { useChangeChunkTextMode } from './hooks'; +import { IChunk, IDslComponent } from './interface'; +interface IProps { + isReadonly: boolean; + isChange: boolean; + setIsChange: (isChange: boolean) => void; + step?: TimelineNode; + data: { value: IDslComponent; key: string }; + reRunLoading: boolean; + clickChunk: (chunk: IChunk) => void; + reRunFunc: (data: { value: IDslComponent; key: string }) => void; +} +const ParserContainer = (props: IProps) => { + const { + isChange, + setIsChange, + step, + data, + reRunFunc, + reRunLoading, + clickChunk, + isReadonly, + } = props; const { t } = useTranslation(); - const { loading } = useFetchParserList(); + const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); + const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); + const initialValue = useMemo(() => { + const outputs = data?.value?.obj?.params?.outputs; + const key = outputs?.output_format?.value; + if (!outputs || !key) return { key: '', type: '', value: [] }; + const value = outputs[key]?.value; + const type = outputs[key]?.type; + console.log('outputs-->', outputs, data, key, value); + return { + key, + type, + value, + }; + }, [data]); const [initialText, setInitialText] = useState(initialValue); - const [isChange, setIsChange] = useState(false); - const handleSave = (newContent: string) => { - console.log('保存内容:', newContent); - if (newContent !== initialText) { + + useEffect(() => { + setInitialText(initialValue); + }, [initialValue]); + const handleSave = (newContent: any) => { + console.log('newContent-change-->', newContent, initialValue); + if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) { setIsChange(true); - onSave(newContent); + setInitialText(newContent); } else { setIsChange(false); } // Here, the API is called to send newContent to the backend }; + + const handleReRunFunc = useCallback(() => { + const newData: { value: IDslComponent; key: string } = { + ...data, + value: { + ...data.value, + obj: { + ...data.value.obj, + params: { + ...(data.value?.obj?.params || {}), + outputs: { + ...(data.value?.obj?.params?.outputs || {}), + [initialText.key]: { + type: initialText.type, + value: initialText.value, + }, + }, + }, + }, + }, + }; + reRunFunc(newData); + setIsChange(false); + }, [data, initialText, reRunFunc, setIsChange]); + + const handleRemoveChunk = useCallback(async () => { + if (selectedChunkIds.length > 0) { + initialText.value = initialText.value.filter( + (item: any, index: number) => !selectedChunkIds.includes(index + ''), + ); + setIsChange(true); + setSelectedChunkIds([]); + } + }, [selectedChunkIds, initialText, setIsChange]); + + const handleCheckboxClick = useCallback( + (id: string | number, checked: boolean) => { + setSelectedChunkIds((prev) => { + if (checked) { + return [...prev, id.toString()]; + } else { + return prev.filter((item) => item.toString() !== id.toString()); + } + }); + }, + [], + ); + + const selectAllChunk = useCallback( + (checked: boolean) => { + setSelectedChunkIds( + checked ? initialText.value.map((x, index: number) => index) : [], + ); + }, + [initialText.value], + ); + + const isChunck = + step?.type === TimelineNodeType.characterSplitter || + step?.type === TimelineNodeType.titleSplitter; + + const handleCreateChunk = useCallback( + (text: string) => { + const newText = [...initialText.value, { text: text || ' ' }]; + setInitialText({ + ...initialText, + value: newText, + }); + }, + [initialText], + ); + return ( <> - {isChange && ( + {isChange && !isReadonly && ( <div className=" absolute top-2 right-6"> - <RerunButton /> + <RerunButton + step={step} + onRerun={handleReRunFunc} + loading={reRunLoading} + /> </div> )} - <div className={classNames('flex flex-col w-3/5')}> - <Spin spinning={loading} className="" size="large"> - <div className="h-[50px] flex flex-col justify-end pb-[5px]"> + <div className={classNames('flex flex-col w-full')}> + {/* <Spin spinning={false} className="" size="large"> */} + <div className="h-[50px] flex flex-col justify-end pb-[5px]"> + {!isChunck && ( <div> <h2 className="text-[16px]"> {t('dataflowParser.parseSummary')} @@ -41,16 +159,63 @@ const ParserContainer = () => { {t('dataflowParser.parseSummaryTip')} </div> </div> + )} + {isChunck && ( + <div> + <h2 className="text-[16px]">{t('chunk.chunkResult')}</h2> + <div className="text-[12px] text-text-secondary italic"> + {t('chunk.chunkResultTip')} + </div> + </div> + )} + </div> + + {isChunck && ( + <div className="pt-[5px] pb-[5px] flex justify-between items-center"> + {!isReadonly && ( + <CheckboxSets + selectAllChunk={selectAllChunk} + removeChunk={handleRemoveChunk} + checked={selectedChunkIds.length === initialText.value.length} + selectedChunkIds={selectedChunkIds} + /> + )} + <ChunkResultBar + isReadonly={isReadonly} + changeChunkTextMode={changeChunkTextMode} + createChunk={handleCreateChunk} + /> </div> - <div className=" border rounded-lg p-[20px] box-border h-[calc(100vh-180px)] overflow-auto scrollbar-none"> - <FormatPreserveEditor + )} + + <div + className={cn( + ' border rounded-lg p-[20px] box-border w-[calc(100%-20px)] overflow-auto scrollbar-none', + { + 'h-[calc(100vh-240px)]': isChunck, + 'h-[calc(100vh-180px)]': !isChunck, + }, + )} + > + {initialText && ( + <FormatPreserEditor initialValue={initialText} onSave={handleSave} - className="!h-[calc(100vh-220px)]" + isReadonly={isReadonly} + isChunck={isChunck} + textMode={textMode} + isDelete={ + step?.type === TimelineNodeType.characterSplitter || + step?.type === TimelineNodeType.titleSplitter + } + clickChunk={clickChunk} + handleCheckboxClick={handleCheckboxClick} + selectedChunkIds={selectedChunkIds} /> - <Spotlight opcity={0.6} coverage={60} /> - </div> - </Spin> + )} + <Spotlight opcity={0.6} coverage={60} /> + </div> + {/* </Spin> */} </div> </> ); diff --git a/web/src/pages/dataset/dataset-overview/dataset-common.ts b/web/src/pages/dataset/dataset-overview/dataset-common.ts index 9581ff6b6..01eaa2b84 100644 --- a/web/src/pages/dataset/dataset-overview/dataset-common.ts +++ b/web/src/pages/dataset/dataset-overview/dataset-common.ts @@ -3,7 +3,12 @@ export enum LogTabs { DATASET_LOGS = 'datasetLogs', } -export enum processingType { - knowledgeGraph = 'knowledgeGraph', - raptor = 'raptor', +export enum ProcessingType { + knowledgeGraph = 'GraphRAG', + raptor = 'RAPTOR', } + +export const ProcessingTypeMap = { + [ProcessingType.knowledgeGraph]: 'Knowledge Graph', + [ProcessingType.raptor]: 'Raptor', +}; diff --git a/web/src/pages/dataset/dataset-overview/hook.ts b/web/src/pages/dataset/dataset-overview/hook.ts new file mode 100644 index 000000000..164910ef3 --- /dev/null +++ b/web/src/pages/dataset/dataset-overview/hook.ts @@ -0,0 +1,96 @@ +import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit'; +import { + useGetPaginationWithRouter, + useHandleSearchChange, +} from '@/hooks/logic-hooks'; +import kbService, { + listDataPipelineLogDocument, + listPipelineDatasetLogs, +} from '@/services/knowledge-service'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useState } from 'react'; +import { useParams, useSearchParams } from 'umi'; +import { LogTabs } from './dataset-common'; +import { IFileLogList, IOverviewTital } from './interface'; + +const useFetchOverviewTital = () => { + const [searchParams] = useSearchParams(); + const { id } = useParams(); + const knowledgeBaseId = searchParams.get('id') || id; + const { data } = useQuery<IOverviewTital>({ + queryKey: ['overviewTital'], + queryFn: async () => { + const { data: res = {} } = await kbService.getKnowledgeBasicInfo({ + kb_id: knowledgeBaseId, + }); + return res.data || []; + }, + }); + return { data }; +}; + +const useFetchFileLogList = () => { + const [searchParams] = useSearchParams(); + const { searchString, handleInputChange } = useHandleSearchChange(); + const { pagination, setPagination } = useGetPaginationWithRouter(); + const { filterValue, handleFilterSubmit } = useHandleFilterSubmit(); + const { id } = useParams(); + const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>( + LogTabs.FILE_LOGS, + ); + const knowledgeBaseId = searchParams.get('id') || id; + const fetchFunc = + active === LogTabs.DATASET_LOGS + ? listPipelineDatasetLogs + : listDataPipelineLogDocument; + const { data } = useQuery<IFileLogList>({ + queryKey: [ + 'fileLogList', + knowledgeBaseId, + pagination, + searchString, + active, + filterValue, + ], + placeholderData: (previousData) => { + if (previousData === undefined) { + return { logs: [], total: 0 }; + } + return previousData; + }, + enabled: true, + queryFn: async () => { + const { data: res = {} } = await fetchFunc( + { + kb_id: knowledgeBaseId, + page: pagination.current, + page_size: pagination.pageSize, + keywords: searchString, + // order_by: '', + }, + { ...filterValue }, + ); + return res.data || []; + }, + }); + const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback( + (e) => { + setPagination({ page: 1 }); + handleInputChange(e); + }, + [handleInputChange, setPagination], + ); + return { + data, + searchString, + handleInputChange: onInputChange, + pagination: { ...pagination, total: data?.total }, + setPagination, + active, + setActive, + filterValue, + handleFilterSubmit, + }; +}; + +export { useFetchFileLogList, useFetchOverviewTital }; diff --git a/web/src/pages/dataset/dataset-overview/index.tsx b/web/src/pages/dataset/dataset-overview/index.tsx index ce1ab29b3..39a89b80d 100644 --- a/web/src/pages/dataset/dataset-overview/index.tsx +++ b/web/src/pages/dataset/dataset-overview/index.tsx @@ -1,13 +1,16 @@ -import { - CircleQuestionMark, - Cpu, - FileChartLine, - HardDriveDownload, -} from 'lucide-react'; -import { FC, useState } from 'react'; +import { FilterCollection } from '@/components/list-filter-bar/interface'; +import SvgIcon from '@/components/svg-icon'; +import { useIsDarkTheme } from '@/components/theme-provider'; +import { AntToolTip } from '@/components/ui/tooltip'; +import { useFetchDocumentList } from '@/hooks/use-document-request'; +import { t } from 'i18next'; +import { CircleQuestionMark } from 'lucide-react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { RunningStatus, RunningStatusMap } from '../dataset/constant'; import { LogTabs } from './dataset-common'; import { DatasetFilter } from './dataset-filter'; +import { useFetchFileLogList, useFetchOverviewTital } from './hook'; import FileLogsTable from './overview-table'; interface StatCardProps { @@ -15,15 +18,30 @@ interface StatCardProps { value: number; icon: JSX.Element; children?: JSX.Element; + tooltip?: string; +} +interface CardFooterProcessProps { + success: number; + failed: number; } -const StatCard: FC<StatCardProps> = ({ title, value, children, icon }) => { +const StatCard: FC<StatCardProps> = ({ + title, + value, + children, + icon, + tooltip, +}) => { return ( <div className="bg-bg-card p-4 rounded-lg border border-border flex flex-col gap-2"> <div className="flex items-center justify-between"> <h3 className="flex items-center gap-1 text-sm font-medium text-text-secondary"> {title} - <CircleQuestionMark size={12} /> + {tooltip && ( + <AntToolTip title={tooltip} trigger="hover"> + <CircleQuestionMark size={12} /> + </AntToolTip> + )} </h3> {icon} </div> @@ -35,115 +53,228 @@ const StatCard: FC<StatCardProps> = ({ title, value, children, icon }) => { ); }; -interface CardFooterProcessProps { - total: number; - completed: number; - success: number; - failed: number; -} const CardFooterProcess: FC<CardFooterProcessProps> = ({ - total, - completed, - success, - failed, + success = 0, + failed = 0, }) => { const { t } = useTranslation(); - const successPrecentage = (success / total) * 100; - const failedPrecentage = (failed / total) * 100; return ( <div className="flex items-center flex-col gap-2"> - <div className="flex justify-between w-full text-sm text-text-secondary"> - <div className="flex items-center gap-2"> - <div className="flex items-center gap-1"> - {success} - <span>{t('knowledgeDetails.success')}</span> + <div className="w-full flex justify-between gap-4 rounded-lg text-sm font-bold text-text-primary"> + <div className="flex items-center justify-between rounded-md w-1/2 p-2 bg-state-success-5"> + <div className="flex items-center rounded-lg gap-1"> + <div className="w-2 h-2 rounded-full bg-state-success "></div> + <div className="font-normal text-text-secondary text-xs"> + {t('knowledgeDetails.success')} + </div> </div> - <div className="flex items-center gap-1"> - {failed} - <span>{t('knowledgeDetails.failed')}</span> + <div>{success || 0}</div> + </div> + <div className="flex items-center justify-between rounded-md w-1/2 bg-state-error-5 p-2"> + <div className="flex items-center rounded-lg gap-1"> + <div className="w-2 h-2 rounded-full bg-state-error"></div> + <div className="font-normal text-text-secondary text-xs"> + {t('knowledgeDetails.failed')} + </div> </div> + <div>{failed || 0}</div> </div> - <div className="flex items-center gap-1"> - {completed} - <span>{t('knowledgeDetails.completed')}</span> - </div> - </div> - <div className="w-full flex rounded-full h-3 bg-bg-card text-sm font-bold text-text-primary"> - <div - className=" rounded-full h-3 bg-accent-primary" - style={{ width: successPrecentage + '%' }} - ></div> - <div - className=" rounded-full h-3 bg-state-error" - style={{ width: failedPrecentage + '%' }} - ></div> </div> </div> ); }; + +const filters = [ + { + field: 'operation_status', + label: t('knowledgeDetails.status'), + list: Object.values(RunningStatus).map((value) => { + // const value = key as RunningStatus; + console.log(value); + return { + id: value, + label: RunningStatusMap[value].label, + }; + }), + }, + { + field: 'types', + label: t('knowledgeDetails.task'), + list: [ + { + id: 'Parse', + label: 'Parse', + }, + { + id: 'Download', + label: 'Download', + }, + ], + }, +]; const FileLogsPage: FC = () => { const { t } = useTranslation(); - const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>( - LogTabs.FILE_LOGS, - ); - const mockData = Array(30) - .fill(0) - .map((_, i) => ({ - id: i === 0 ? '#952734' : `14`, - fileName: 'PRD for DealBees 1.2 (1).txt', - source: 'GitHub', - pipeline: i === 0 ? 'data demo for...' : i === 1 ? 'test' : 'kiki’s demo', - startDate: '14/03/2025 14:53:39', - task: i === 0 ? 'Parse' : 'Parser', - status: - i === 0 - ? 'Success' - : i === 1 - ? 'Failed' - : i === 2 - ? 'Running' - : 'Pending', - })); - const pagination = { - current: 1, - pageSize: 30, - total: 100, - }; + const [topAllData, setTopAllData] = useState({ + totalFiles: { + value: 0, + precent: 0, + }, + downloads: { + value: 0, + success: 0, + failed: 0, + }, + processing: { + value: 0, + success: 0, + failed: 0, + }, + }); + const { data: topData } = useFetchOverviewTital(); + const { + pagination: { total: fileTotal }, + } = useFetchDocumentList(); + + useEffect(() => { + setTopAllData((prev) => { + return { + ...prev, + processing: { + value: topData?.processing || 0, + success: topData?.finished || 0, + failed: topData?.failed || 0, + }, + }; + }); + }, [topData]); + + useEffect(() => { + setTopAllData((prev) => { + return { + ...prev, + totalFiles: { + value: fileTotal || 0, + precent: 0, + }, + }; + }); + }, [fileTotal]); + + const { + data: tableOriginData, + searchString, + handleInputChange, + pagination, + setPagination, + active, + filterValue, + handleFilterSubmit, + setActive, + } = useFetchFileLogList(); + + const tableList = useMemo(() => { + console.log('tableList', tableOriginData); + if (tableOriginData && tableOriginData.logs?.length) { + return tableOriginData.logs.map((item) => { + return { + ...item, + fileName: item.document_name, + statusName: item.operation_status, + }; + }); + } + }, [tableOriginData]); const changeActiveLogs = (active: (typeof LogTabs)[keyof typeof LogTabs]) => { setActive(active); }; const handlePaginationChange = (page: number, pageSize: number) => { console.log('Pagination changed:', { page, pageSize }); + setPagination({ + ...pagination, + page, + pageSize: pageSize, + }); }; + const isDark = useIsDarkTheme(); + return ( <div className="p-5 min-w-[880px] border-border border rounded-lg mr-5"> {/* Stats Cards */} <div className="grid grid-cols-3 md:grid-cols-3 gap-4 mb-6"> - <StatCard title="Total Files" value={2827} icon={<FileChartLine />}> - <div>+7% from last week</div> + <StatCard + title={t('datasetOverview.totalFiles')} + value={topAllData.totalFiles.value} + icon={ + isDark ? ( + <SvgIcon name="data-flow/total-files-icon" width={40} /> + ) : ( + <SvgIcon name="data-flow/total-files-icon-bri" width={40} /> + ) + } + > + <div> + <span className="text-accent-primary"> + {topAllData.totalFiles.precent > 0 ? '+' : ''} + {topAllData.totalFiles.precent}%{' '} + </span> + <span className="font-normal text-text-secondary text-xs"> + from last week + </span> + </div> </StatCard> - <StatCard title="Downloading" value={28} icon={<HardDriveDownload />}> + <StatCard + title={t('datasetOverview.downloading')} + value={topAllData.downloads.value} + icon={ + isDark ? ( + <SvgIcon name="data-flow/data-icon" width={40} /> + ) : ( + <SvgIcon name="data-flow/data-icon-bri" width={40} /> + ) + } + tooltip={t('datasetOverview.downloadTip')} + > <CardFooterProcess - total={100} - success={8} - failed={2} - completed={15} + success={topAllData.downloads.success} + failed={topAllData.downloads.failed} /> </StatCard> - <StatCard title="Processing" value={156} icon={<Cpu />}> - <CardFooterProcess total={20} success={8} failed={2} completed={15} /> + <StatCard + title={t('datasetOverview.processing')} + value={topAllData.processing.value} + icon={ + isDark ? ( + <SvgIcon name="data-flow/processing-icon" width={40} /> + ) : ( + <SvgIcon name="data-flow/processing-icon-bri" width={40} /> + ) + } + tooltip={t('datasetOverview.processingTip')} + > + <CardFooterProcess + success={topAllData.processing.success} + failed={topAllData.processing.failed} + /> </StatCard> </div> {/* Tabs & Search */} - <DatasetFilter active={active} setActive={changeActiveLogs} /> + <DatasetFilter + filters={filters as FilterCollection[]} + value={filterValue} + active={active} + setActive={changeActiveLogs} + searchString={searchString} + onSearchChange={handleInputChange} + onChange={handleFilterSubmit} + /> {/* Table */} <FileLogsTable - data={mockData} + data={tableList} pagination={pagination} setPagination={handlePaginationChange} pageCount={10} diff --git a/web/src/pages/dataset/dataset-overview/interface.ts b/web/src/pages/dataset/dataset-overview/interface.ts new file mode 100644 index 000000000..1ed5e9948 --- /dev/null +++ b/web/src/pages/dataset/dataset-overview/interface.ts @@ -0,0 +1,62 @@ +import { RunningStatus, RunningStatusMap } from '../dataset/constant'; +import { LogTabs } from './dataset-common'; + +export interface DocumentLog { + fileName: string; + status: RunningStatus; + statusName: typeof RunningStatusMap; +} + +export interface FileLogsTableProps { + data: Array<IFileLogItem & DocumentLog>; + pageCount: number; + pagination: { + current: number; + pageSize: number; + total: number; + }; + setPagination: (pagination: { page: number; pageSize: number }) => void; + loading?: boolean; + active: (typeof LogTabs)[keyof typeof LogTabs]; +} + +export interface IOverviewTital { + cancelled: number; + failed: number; + finished: number; + processing: number; +} + +export interface IFileLogItem { + create_date: string; + create_time: number; + document_id: string; + document_name: string; + document_suffix: string; + document_type: string; + dsl: any; + path: string[]; + task_id: string; + id: string; + name: string; + kb_id: string; + operation_status: string; + parser_id: string; + pipeline_id: string; + pipeline_title: string; + avatar: string; + process_begin_at: null | string; + process_duration: number; + progress: number; + progress_msg: string; + source_from: string; + status: string; + task_type: string; + tenant_id: string; + update_date: string; + update_time: number; +} +export interface IFileLogList { + logs: IFileLogItem[]; + total: number; +} diff --git a/web/src/pages/dataset/dataset-overview/overview-table.tsx b/web/src/pages/dataset/dataset-overview/overview-table.tsx index d02eae615..993d39dbf 100644 --- a/web/src/pages/dataset/dataset-overview/overview-table.tsx +++ b/web/src/pages/dataset/dataset-overview/overview-table.tsx @@ -1,7 +1,6 @@ import FileStatusBadge from '@/components/file-status-badge'; -import { FileIcon } from '@/components/icon-font'; +import { FileIcon, IconFontFill } from '@/components/icon-font'; import { RAGFlowAvatar } from '@/components/ragflow-avatar'; -import SvgIcon from '@/components/svg-icon'; import { Button } from '@/components/ui/button'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { @@ -12,12 +11,16 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { RunningStatusMap } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; -import ProcessLogModal from '@/pages/datasets/process-log-modal'; +import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant'; +import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface'; +import { formatDate, formatSecondsToHumanReadable } from '@/utils/date'; import { ColumnDef, ColumnFiltersState, + Row, SortingState, flexRender, getCoreRowModel, @@ -27,62 +30,43 @@ import { useReactTable, } from '@tanstack/react-table'; import { TFunction } from 'i18next'; -import { ClipboardList, Eye } from 'lucide-react'; -import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react'; -import { LogTabs, processingType } from './dataset-common'; - -interface DocumentLog { - id: string; - fileName: string; - source: string; - pipeline: string; - startDate: string; - task: string; - status: 'Success' | 'Failed' | 'Running' | 'Pending'; -} - -interface FileLogsTableProps { - data: DocumentLog[]; - pageCount: number; - pagination: { - current: number; - pageSize: number; - total: number; - }; - setPagination: (pagination: { page: number; pageSize: number }) => void; - loading?: boolean; - active: (typeof LogTabs)[keyof typeof LogTabs]; -} +import { ArrowUpDown, ClipboardList, Eye } from 'lucide-react'; +import { FC, useMemo, useState } from 'react'; +import { useParams } from 'umi'; +import { RunningStatus } from '../dataset/constant'; +import ProcessLogModal from '../process-log-modal'; +import { LogTabs, ProcessingType, ProcessingTypeMap } from './dataset-common'; +import { DocumentLog, FileLogsTableProps, IFileLogItem } from './interface'; export const getFileLogsTableColumns = ( t: TFunction<'translation', string>, - setIsModalVisible: Dispatch<SetStateAction<boolean>>, + showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void, + kowledgeId: string, navigateToDataflowResult: ( - id: string, - knowledgeId?: string | undefined, + props: NavigateToDataflowResultProps, ) => () => void, ) => { // const { t } = useTranslate('knowledgeDetails'); - const columns: ColumnDef<DocumentLog>[] = [ - { - id: 'select', - header: ({ table }) => ( - <input - type="checkbox" - checked={table.getIsAllRowsSelected()} - onChange={table.getToggleAllRowsSelectedHandler()} - className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500" - /> - ), - cell: ({ row }) => ( - <input - type="checkbox" - checked={row.getIsSelected()} - onChange={row.getToggleSelectedHandler()} - className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500" - /> - ), - }, + const columns: ColumnDef<IFileLogItem & DocumentLog>[] = [ + // { + // id: 'select', + // header: ({ table }) => ( + // <input + // type="checkbox" + // checked={table.getIsAllRowsSelected()} + // onChange={table.getToggleAllRowsSelectedHandler()} + // className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500" + // /> + // ), + // cell: ({ row }) => ( + // <input + // type="checkbox" + // checked={row.getIsSelected()} + // onChange={row.getToggleSelectedHandler()} + // className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500" + // /> + // ), + // }, { accessorKey: 'id', header: 'ID', @@ -96,10 +80,10 @@ export const getFileLogsTableColumns = ( cell: ({ row }) => ( <div className="flex items-center gap-2 text-text-primary" - onClick={navigateToDataflowResult( - row.original.id, - row.original.kb_id, - )} + // onClick={navigateToDataflowResult( + // row.original.id, + // row.original.kb_id, + // )} > <FileIcon name={row.original.fileName}></FileIcon> {row.original.fileName} @@ -107,68 +91,97 @@ export const getFileLogsTableColumns = ( ), }, { - accessorKey: 'source', + accessorKey: 'source_from', header: t('source'), cell: ({ row }) => ( - <div className="text-text-primary">{row.original.source}</div> + <div className="text-text-primary">{row.original.source_from}</div> ), }, { - accessorKey: 'pipeline', + accessorKey: 'pipeline_title', header: t('dataPipeline'), cell: ({ row }) => ( <div className="flex items-center gap-2 text-text-primary"> <RAGFlowAvatar - avatar={null} - name={row.original.pipeline} + avatar={row.original.avatar} + name={row.original.pipeline_title} className="size-4" /> - {row.original.pipeline} + {row.original.pipeline_title} </div> ), }, { - accessorKey: 'startDate', - header: t('startDate'), + accessorKey: 'process_begin_at', + header: ({ column }) => { + return ( + <Button + variant="transparent" + className="border-none" + onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} + > + {t('startDate')} + <ArrowUpDown /> + </Button> + ); + }, cell: ({ row }) => ( - <div className="text-text-primary">{row.original.startDate}</div> + <div className="text-text-primary"> + {formatDate(row.original.process_begin_at)} + </div> ), }, { - accessorKey: 'task', + accessorKey: 'task_type', header: t('task'), cell: ({ row }) => ( - <div className="text-text-primary">{row.original.task}</div> + <div className="text-text-primary">{row.original.task_type}</div> ), }, { - accessorKey: 'status', + accessorKey: 'operation_status', header: t('status'), - cell: ({ row }) => <FileStatusBadge status={row.original.status} />, + cell: ({ row }) => ( + <FileStatusBadge + status={row.original.operation_status as RunningStatus} + name={ + RunningStatusMap[row.original.operation_status as RunningStatus] + } + /> + ), }, { id: 'operations', header: t('operations'), cell: ({ row }) => ( - <div className="flex justify-start space-x-2"> + <div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity"> <Button variant="ghost" size="sm" className="p-1" onClick={() => { - setIsModalVisible(true); + showLog(row, LogTabs.FILE_LOGS); }} > <Eye /> </Button> - <Button - variant="ghost" - size="sm" - className="p-1" - onClick={navigateToDataflowResult(row.original.id)} - > - <ClipboardList /> - </Button> + {row.original.pipeline_id && ( + <Button + variant="ghost" + size="sm" + className="p-1" + onClick={navigateToDataflowResult({ + id: row.original.id, + [PipelineResultSearchParams.KnowledgeId]: kowledgeId, + [PipelineResultSearchParams.DocumentId]: + row.original.document_id, + [PipelineResultSearchParams.IsReadOnly]: 'false', + [PipelineResultSearchParams.Type]: 'dataflow', + })} + > + <ClipboardList /> + </Button> + )} </div> ), }, @@ -179,29 +192,29 @@ export const getFileLogsTableColumns = ( export const getDatasetLogsTableColumns = ( t: TFunction<'translation', string>, - setIsModalVisible: Dispatch<SetStateAction<boolean>>, + showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void, ) => { // const { t } = useTranslate('knowledgeDetails'); - const columns: ColumnDef<DocumentLog>[] = [ - { - id: 'select', - header: ({ table }) => ( - <input - type="checkbox" - checked={table.getIsAllRowsSelected()} - onChange={table.getToggleAllRowsSelectedHandler()} - className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500" - /> - ), - cell: ({ row }) => ( - <input - type="checkbox" - checked={row.getIsSelected()} - onChange={row.getToggleSelectedHandler()} - className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500" - /> - ), - }, + const columns: ColumnDef<IFileLogItem & DocumentLog>[] = [ + // { + // id: 'select', + // header: ({ table }) => ( + // <input + // type="checkbox" + // checked={table.getIsAllRowsSelected()} + // onChange={table.getToggleAllRowsSelectedHandler()} + // className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500" + // /> + // ), + // cell: ({ row }) => ( + // <input + // type="checkbox" + // checked={row.getIsSelected()} + // onChange={row.getToggleSelectedHandler()} + // className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500" + // /> + // ), + // }, { accessorKey: 'id', header: 'ID', @@ -210,43 +223,74 @@ export const getDatasetLogsTableColumns = ( ), }, { - accessorKey: 'startDate', - header: t('startDate'), + accessorKey: 'process_begin_at', + header: ({ column }) => { + return ( + <Button + variant="transparent" + className="border-none" + onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} + > + {t('startDate')} + <ArrowUpDown /> + </Button> + ); + }, cell: ({ row }) => ( - <div className="text-text-primary">{row.original.startDate}</div> - ), - }, - { - accessorKey: 'processingType', - header: t('processingType'), - cell: ({ row }) => ( - <div className="flex items-center gap-2 text-text-primary"> - {processingType.knowledgeGraph === row.original.processingType && ( - <SvgIcon name={`data-flow/knowledgegraph`} width={24}></SvgIcon> - )} - {processingType.raptor === row.original.processingType && ( - <SvgIcon name={`data-flow/raptor`} width={24}></SvgIcon> - )} - {row.original.processingType} + <div className="text-text-primary"> + {formatDate(row.original.process_begin_at)} </div> ), }, { - accessorKey: 'status', + accessorKey: 'task_type', + header: t('processingType'), + cell: ({ row }) => ( + <div className="flex items-center gap-2 text-text-primary"> + {ProcessingType.knowledgeGraph === row.original.task_type && ( + <IconFontFill + name={`knowledgegraph`} + className="text-text-secondary" + ></IconFontFill> + )} + {ProcessingType.raptor === row.original.task_type && ( + <IconFontFill + name={`dataflow-01`} + className="text-text-secondary" + ></IconFontFill> + )} + {ProcessingTypeMap[row.original.task_type as ProcessingType] || + row.original.task_type} + </div> + ), + }, + { + accessorKey: 'operation_status', header: t('status'), - cell: ({ row }) => <FileStatusBadge status={row.original.status} />, + cell: ({ row }) => ( + // <FileStatusBadge + // status={row.original.status} + // name={row.original.statusName} + // /> + <FileStatusBadge + status={row.original.operation_status as RunningStatus} + name={ + RunningStatusMap[row.original.operation_status as RunningStatus] + } + /> + ), }, { id: 'operations', header: t('operations'), cell: ({ row }) => ( - <div className="flex justify-start space-x-2"> + <div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity"> <Button variant="ghost" size="sm" className="p-1" onClick={() => { - setIsModalVisible(true); + showLog(row, LogTabs.DATASET_LOGS); }} > <Eye /> @@ -272,11 +316,35 @@ const FileLogsTable: FC<FileLogsTableProps> = ({ const { t } = useTranslate('knowledgeDetails'); const [isModalVisible, setIsModalVisible] = useState(false); const { navigateToDataflowResult } = useNavigatePage(); + const [logInfo, setLogInfo] = useState<IFileLogItem>(); + const kowledgeId = useParams().id; + const showLog = (row: Row<IFileLogItem & DocumentLog>) => { + const logDetail = { + taskId: row.original?.dsl?.task_id, + fileName: row.original.document_name, + source: row.original.source_from, + task: row.original?.task_type, + status: row.original.statusName, + startDate: formatDate(row.original.process_begin_at), + duration: formatSecondsToHumanReadable( + row.original.process_duration || 0, + ), + details: row.original.progress_msg, + }; + console.log('logDetail', logDetail); + setLogInfo(logDetail); + setIsModalVisible(true); + }; + const columns = useMemo(() => { - console.log('columns', active); return active === LogTabs.FILE_LOGS - ? getFileLogsTableColumns(t, setIsModalVisible, navigateToDataflowResult) - : getDatasetLogsTableColumns(t, setIsModalVisible); + ? getFileLogsTableColumns( + t, + showLog, + kowledgeId || '', + navigateToDataflowResult, + ) + : getDatasetLogsTableColumns(t, showLog); }, [active, t]); const currentPagination = useMemo( @@ -287,8 +355,8 @@ const FileLogsTable: FC<FileLogsTableProps> = ({ [pagination], ); - const table = useReactTable({ - data, + const table = useReactTable<IFileLogItem & DocumentLog>({ + data: data || [], columns, manualPagination: true, getCoreRowModel: getCoreRowModel(), @@ -308,19 +376,9 @@ const FileLogsTable: FC<FileLogsTableProps> = ({ ? Math.ceil(pagination.total / pagination.pageSize) : 0, }); - const taskInfo = { - taskId: '#9527', - fileName: 'PRD for DealBees 1.2 (1).text', - fileSize: '2.4G', - source: 'Github', - task: 'Parse', - state: 'Running', - startTime: '14/03/2025 14:53:39', - duration: '800', - details: 'PRD for DealBees 1.2 (1).text', - }; + return ( - <div className="w-full h-[calc(100vh-350px)]"> + <div className="w-full h-[calc(100vh-360px)]"> <Table rootClassName="max-h-[calc(100vh-380px)]"> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( @@ -337,7 +395,7 @@ const FileLogsTable: FC<FileLogsTableProps> = ({ ))} </TableHeader> <TableBody className="relative"> - {table.getRowModel().rows.length ? ( + {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( <TableRow key={row.id} @@ -363,7 +421,7 @@ const FileLogsTable: FC<FileLogsTableProps> = ({ )} </TableBody> </Table> - <div className="flex items-center justify-end py-4 absolute bottom-3 right-12"> + <div className="flex items-center justify-end absolute bottom-3 right-12"> <div className="space-x-2"> <RAGFlowPagination {...{ current: pagination.current, pageSize: pagination.pageSize }} @@ -372,11 +430,14 @@ const FileLogsTable: FC<FileLogsTableProps> = ({ /> </div> </div> - <ProcessLogModal - visible={isModalVisible} - onCancel={() => setIsModalVisible(false)} - taskInfo={taskInfo} - /> + {isModalVisible && ( + <ProcessLogModal + title={active === LogTabs.FILE_LOGS ? t('fileLogs') : t('datasetLog')} + visible={isModalVisible} + onCancel={() => setIsModalVisible(false)} + logInfo={logInfo} + /> + )} </div> ); }; diff --git a/web/src/pages/dataset/dataset-setting/chunk-method-form.tsx b/web/src/pages/dataset/dataset-setting/chunk-method-form.tsx deleted file mode 100644 index 62462a29d..000000000 --- a/web/src/pages/dataset/dataset-setting/chunk-method-form.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import { NaiveConfiguration } from './naive'; -import { SavingButton } from './saving-button'; - -export function ChunkMethodForm() { - const form = useFormContext(); - const { t } = useTranslation(); - - return ( - <section className="h-full flex flex-col"> - <div className="overflow-auto flex-1 min-h-0"> - <NaiveConfiguration></NaiveConfiguration> - </div> - <div className="text-right pt-4 flex justify-end gap-3"> - <Button - type="reset" - className="bg-transparent text-color-white hover:bg-transparent border-gray-500 border-[1px]" - onClick={() => { - form.reset(); - }} - > - {t('knowledgeConfiguration.cancel')} - </Button> - <SavingButton></SavingButton> - </div> - </section> - ); -} diff --git a/web/src/pages/dataset/dataset-setting/components/link-data-pipeline.tsx b/web/src/pages/dataset/dataset-setting/components/link-data-pipeline.tsx new file mode 100644 index 000000000..92c80dab6 --- /dev/null +++ b/web/src/pages/dataset/dataset-setting/components/link-data-pipeline.tsx @@ -0,0 +1,191 @@ +import { IDataPipelineSelectNode } from '@/components/data-pipeline-select'; +import { IconFont } from '@/components/icon-font'; +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import { Button } from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal/modal'; +import { Link } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import LinkDataPipelineModal from './link-data-pipline-modal'; +export interface IDataPipelineNodeProps extends IDataPipelineSelectNode { + isDefault?: boolean; + linked?: boolean; +} + +export interface ILinkDataPipelineProps { + data?: IDataPipelineNodeProps; + handleLinkOrEditSubmit?: (data: IDataPipelineNodeProps | undefined) => void; +} + +interface DataPipelineItemProps extends IDataPipelineNodeProps { + openLinkModalFunc?: (open: boolean, data?: IDataPipelineNodeProps) => void; +} + +const DataPipelineItem = (props: DataPipelineItemProps) => { + const { t } = useTranslation(); + const { name, avatar, isDefault, linked, openLinkModalFunc } = props; + const openUnlinkModal = () => { + Modal.show({ + visible: true, + className: '!w-[560px]', + title: t('dataflowParser.unlinkPipelineModalTitle'), + children: ( + <div + className="text-sm text-text-secondary" + dangerouslySetInnerHTML={{ + __html: t('dataflowParser.unlinkPipelineModalContent'), + }} + ></div> + ), + onVisibleChange: () => { + Modal.hide(); + }, + footer: ( + <div className="flex justify-end gap-2"> + <Button variant={'outline'} onClick={() => Modal.hide()}> + {t('dataflowParser.changeStepModalCancelText')} + </Button> + <Button + variant={'secondary'} + className="!bg-state-error text-bg-base" + onClick={() => { + Modal.hide(); + }} + > + {t('dataflowParser.unlinkPipelineModalConfirmText')} + </Button> + </div> + ), + }); + }; + + return ( + <div className="flex items-center justify-between gap-1 px-2 rounded-md border"> + <div className="flex items-center gap-1"> + <RAGFlowAvatar avatar={avatar} name={name} className="size-4" /> + <div>{name}</div> + {/* {isDefault && ( + <div className="text-xs bg-text-secondary text-bg-base px-2 py-1 rounded-md"> + {t('knowledgeConfiguration.default')} + </div> + )} */} + </div> + {/* <div className="flex gap-1 items-center"> + <Button + variant={'transparent'} + className="border-none" + type="button" + onClick={() => + openLinkModalFunc?.(true, { ...omit(props, ['openLinkModalFunc']) }) + } + > + <Settings2 /> + </Button> + {!isDefault && ( + <> + {linked && ( + <Button + type="button" + variant={'transparent'} + className="border-none" + onClick={() => { + openUnlinkModal(); + }} + > + <Unlink /> + </Button> + )} + </> + )} + </div> */} + </div> + ); +}; + +const LinkDataPipeline = (props: ILinkDataPipelineProps) => { + const { data, handleLinkOrEditSubmit: submit } = props; + const { t } = useTranslation(); + const [openLinkModal, setOpenLinkModal] = useState(false); + const [currentDataPipeline, setCurrentDataPipeline] = + useState<IDataPipelineNodeProps>(); + const pipelineNode: IDataPipelineNodeProps[] = useMemo( + () => [ + { + id: data?.id, + name: data?.name, + avatar: data?.avatar, + isDefault: data?.isDefault, + linked: true, + }, + ], + [data], + ); + const openLinkModalFunc = (open: boolean, data?: IDataPipelineNodeProps) => { + console.log('open', open, data); + setOpenLinkModal(open); + if (data) { + setCurrentDataPipeline(data); + } else { + setCurrentDataPipeline(undefined); + } + }; + const handleLinkOrEditSubmit = ( + data: IDataPipelineSelectNode | undefined, + ) => { + console.log('handleLinkOrEditSubmit', data); + submit?.(data); + setOpenLinkModal(false); + }; + return ( + <div className="flex flex-col gap-2"> + <section className="flex flex-col"> + <div className="flex items-center gap-1 text-text-primary text-sm"> + <IconFont name="Pipeline" /> + {t('knowledgeConfiguration.dataPipeline')} + </div> + <div className="flex justify-between items-center"> + <div className="text-center text-xs text-text-secondary"> + {t('knowledgeConfiguration.linkPipelineSetTip')} + </div> + <Button + type="button" + variant={'transparent'} + onClick={() => { + openLinkModalFunc?.(true); + }} + > + <Link /> + <span className="text-xs text-text-primary"> + {t('knowledgeConfiguration.linkDataPipeline')} + </span> + </Button> + </div> + </section> + <section className="flex flex-col gap-2"> + {pipelineNode.map( + (item) => + item.id && ( + <DataPipelineItem + key={item.id} + openLinkModalFunc={openLinkModalFunc} + id={item.id} + name={item.name} + avatar={item.avatar} + isDefault={item.isDefault} + linked={item.linked} + /> + ), + )} + </section> + <LinkDataPipelineModal + data={currentDataPipeline} + open={openLinkModal} + setOpen={(open: boolean) => { + openLinkModalFunc(open); + }} + onSubmit={handleLinkOrEditSubmit} + /> + </div> + ); +}; +export default LinkDataPipeline; diff --git a/web/src/pages/dataset/dataset-setting/components/link-data-pipline-modal.tsx b/web/src/pages/dataset/dataset-setting/components/link-data-pipline-modal.tsx new file mode 100644 index 000000000..3c8144c6d --- /dev/null +++ b/web/src/pages/dataset/dataset-setting/components/link-data-pipline-modal.tsx @@ -0,0 +1,163 @@ +import { + DataFlowSelect, + IDataPipelineSelectNode, +} from '@/components/data-pipeline-select'; +import { Button } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { Modal } from '@/components/ui/modal/modal'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { t } from 'i18next'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { pipelineFormSchema } from '../form-schema'; +import { IDataPipelineNodeProps } from './link-data-pipeline'; + +const LinkDataPipelineModal = ({ + data, + open, + setOpen, + onSubmit, +}: { + data: IDataPipelineNodeProps | undefined; + open: boolean; + setOpen: (open: boolean) => void; + onSubmit?: (pipeline: IDataPipelineSelectNode | undefined) => void; +}) => { + const isEdit = !!data; + const [list, setList] = useState<IDataPipelineSelectNode[]>(); + const form = useForm<z.infer<typeof pipelineFormSchema>>({ + resolver: zodResolver(pipelineFormSchema), + defaultValues: { + pipeline_id: '', + set_default: false, + file_filter: '', + }, + }); + // const [open, setOpen] = useState(false); + const { navigateToAgents } = useNavigatePage(); + const handleFormSubmit = (values: any) => { + console.log(values, data); + // const param = { + // ...data, + // ...values, + // }; + const pipeline = list?.find((item) => item.id === values.pipeline_id); + onSubmit?.(pipeline); + }; + return ( + <Modal + className="!w-[560px]" + title={ + !isEdit + ? t('knowledgeConfiguration.linkDataPipeline') + : t('knowledgeConfiguration.eidtLinkDataPipeline') + } + open={open} + onOpenChange={setOpen} + showfooter={false} + > + <Form {...form}> + <form onSubmit={form.handleSubmit(handleFormSubmit)}> + <div className="flex flex-col gap-4 "> + {!isEdit && ( + <DataFlowSelect + toDataPipeline={navigateToAgents} + formFieldName="pipeline_id" + setDataList={setList} + /> + )} + {/* <FormField + control={form.control} + name={'file_filter'} + render={({ field }) => ( + <FormItem className=" items-center space-y-0 "> + <div className="flex flex-col gap-1"> + <div className="flex gap-2 justify-between "> + <FormLabel + tooltip={t('knowledgeConfiguration.fileFilterTip')} + className="text-sm text-text-primary whitespace-wrap " + > + {t('knowledgeConfiguration.fileFilter')} + </FormLabel> + </div> + + <div className="text-muted-foreground"> + <FormControl> + <Input + placeholder={t( + 'knowledgeConfiguration.filterPlaceholder', + )} + {...field} + /> + </FormControl> + </div> + </div> + <div className="flex pt-1"> + <div className="w-full"></div> + <FormMessage /> + </div> + </FormItem> + )} + /> + {isEdit && ( + <FormField + control={form.control} + name={'set_default'} + render={({ field }) => ( + <FormItem className=" items-center space-y-0 "> + <div className="flex flex-col gap-1"> + <div className="flex gap-2 justify-between "> + <FormLabel + tooltip={t('knowledgeConfiguration.setDefaultTip')} + className="text-sm text-text-primary whitespace-wrap " + > + {t('knowledgeConfiguration.setDefault')} + </FormLabel> + </div> + + <div className="text-muted-foreground"> + <FormControl> + <Switch + value={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + </div> + </div> + <div className="flex pt-1"> + <div className="w-full"></div> + <FormMessage /> + </div> + </FormItem> + )} + /> + )} */} + <div className="flex justify-end gap-1"> + <Button + type="button" + variant={'outline'} + className="btn-primary" + onClick={() => { + setOpen(false); + }} + > + {t('modal.cancelText')} + </Button> + <Button + type="button" + variant={'default'} + className="btn-primary" + onClick={form.handleSubmit(handleFormSubmit)} + > + {t('modal.okText')} + </Button> + </div> + </div> + </form> + </Form> + </Modal> + ); +}; +export default LinkDataPipelineModal; diff --git a/web/src/pages/dataset/dataset-setting/configuration-form-container.tsx b/web/src/pages/dataset/dataset-setting/configuration-form-container.tsx index 93e30ffb7..f262e3ea8 100644 --- a/web/src/pages/dataset/dataset-setting/configuration-form-container.tsx +++ b/web/src/pages/dataset/dataset-setting/configuration-form-container.tsx @@ -9,6 +9,9 @@ export function ConfigurationFormContainer({ return <section className={cn('space-y-4', className)}>{children}</section>; } -export function MainContainer({ children }: PropsWithChildren) { - return <section className="space-y-5">{children}</section>; +export function MainContainer({ + children, + className, +}: PropsWithChildren & { className?: string }) { + return <section className={cn('space-y-5', className)}>{children}</section>; } diff --git a/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx b/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx index 8b2e3c589..a92937e3e 100644 --- a/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx +++ b/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx @@ -1,3 +1,4 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; import { FormControl, FormField, @@ -10,15 +11,18 @@ import { RAGFlowSelect } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { useTranslate } from '@/hooks/common-hooks'; import { cn } from '@/lib/utils'; -import { ArrowUpRight } from 'lucide-react'; import { useFormContext } from 'react-hook-form'; import { useHasParsedDocument, useSelectChunkMethodList, useSelectEmbeddingModelOptions, } from '../hooks'; - -export function ChunkMethodItem() { +interface IProps { + line?: 1 | 2; + isEdit?: boolean; +} +export function ChunkMethodItem(props: IProps) { + const { line } = props; const { t } = useTranslate('knowledgeConfiguration'); const form = useFormContext(); // const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form); @@ -29,28 +33,29 @@ export function ChunkMethodItem() { control={form.control} name={'parser_id'} render={({ field }) => ( - <FormItem className=" items-center space-y-0 "> - <div className="flex items-center"> + <FormItem className=" items-center space-y-1"> + <div className={line === 1 ? 'flex items-center' : ''}> <FormLabel required tooltip={t('chunkMethodTip')} - className="text-sm text-muted-foreground whitespace-wrap w-1/4" + className={cn('text-sm', { + 'w-1/4 whitespace-pre-wrap': line === 1, + })} > - {t('chunkMethod')} + {t('dataPipeline')} </FormLabel> - <div className="w-3/4 "> + <div className={line === 1 ? 'w-3/4 ' : 'w-full'}> <FormControl> <RAGFlowSelect {...field} options={parserList} placeholder={t('chunkMethodPlaceholder')} - // onChange={handleChunkMethodSelectChange} /> </FormControl> </div> </div> <div className="flex pt-1"> - <div className="w-1/4"></div> + <div className={line === 1 ? 'w-1/4' : ''}></div> <FormMessage /> </div> </FormItem> @@ -58,49 +63,55 @@ export function ChunkMethodItem() { /> ); } - -export function EmbeddingModelItem({ line = 1 }: { line?: 1 | 2 }) { +export function EmbeddingModelItem({ line = 1, isEdit = true }: IProps) { const { t } = useTranslate('knowledgeConfiguration'); const form = useFormContext(); const embeddingModelOptions = useSelectEmbeddingModelOptions(); - const disabled = useHasParsedDocument(); - + const disabled = useHasParsedDocument(isEdit); return ( - <FormField - control={form.control} - name={'embd_id'} - render={({ field }) => ( - <FormItem className=" items-center space-y-0 "> - <div className={cn({ 'flex items-center': line === 1 })}> - <FormLabel - required - tooltip={t('embeddingModelTip')} - className={cn('text-sm whitespace-wrap ', { - 'w-1/4': line === 1, + <> + <FormField + control={form.control} + name={'embd_id'} + render={({ field }) => ( + <FormItem className={cn(' items-center space-y-0 ')}> + <div + className={cn('flex', { + ' items-center': line === 1, + 'flex-col gap-1': line === 2, })} > - {t('embeddingModel')} - </FormLabel> - <div - className={cn('text-muted-foreground', { 'w-3/4': line === 1 })} - > - <FormControl> - <RAGFlowSelect - {...field} - options={embeddingModelOptions} - disabled={disabled} - placeholder={t('embeddingModelPlaceholder')} - /> - </FormControl> + <FormLabel + required + tooltip={t('embeddingModelTip')} + className={cn('text-sm whitespace-wrap ', { + 'w-1/4': line === 1, + })} + > + {t('embeddingModel')} + </FormLabel> + <div + className={cn('text-muted-foreground', { 'w-3/4': line === 1 })} + > + <FormControl> + <SelectWithSearch + onChange={field.onChange} + value={field.value} + options={embeddingModelOptions} + disabled={isEdit ? disabled : false} + placeholder={t('embeddingModelPlaceholder')} + /> + </FormControl> + </div> </div> - </div> - <div className="flex pt-1"> - <div className="w-1/4"></div> - <FormMessage /> - </div> - </FormItem> - )} - /> + <div className="flex pt-1"> + <div className={line === 1 ? 'w-1/4' : ''}></div> + <FormMessage /> + </div> + </FormItem> + )} + /> + </> ); } @@ -142,155 +153,6 @@ export function ParseTypeItem() { ); } -export function DataFlowItem() { - const { t } = useTranslate('knowledgeConfiguration'); - const form = useFormContext(); - - return ( - <FormField - control={form.control} - name={'data_flow'} - render={({ field }) => ( - <FormItem className=" items-center space-y-0 "> - <div className=""> - <div className="flex gap-2 justify-between "> - <FormLabel - tooltip={t('dataFlowTip')} - className="text-sm text-text-primary whitespace-wrap " - > - {t('dataFlow')} - </FormLabel> - <div className="text-sm flex text-text-primary"> - {t('buildItFromScratch')} - <ArrowUpRight size={14} /> - </div> - </div> - - <div className="text-muted-foreground"> - <FormControl> - <RAGFlowSelect - {...field} - placeholder={t('dataFlowPlaceholder')} - options={[{ value: '0', label: t('dataFlowDefault') }]} - /> - </FormControl> - </div> - </div> - <div className="flex pt-1"> - <div className="w-1/4"></div> - <FormMessage /> - </div> - </FormItem> - )} - /> - ); -} - -export function DataExtractKnowledgeItem() { - const { t } = useTranslate('knowledgeConfiguration'); - const form = useFormContext(); - - return ( - <> - {' '} - <FormField - control={form.control} - name={'extractKnowledgeGraph'} - render={({ field }) => ( - <FormItem className=" items-center space-y-0 "> - <div className=""> - <FormLabel - tooltip={t('extractKnowledgeGraphTip')} - className="text-sm whitespace-wrap " - > - {t('extractKnowledgeGraph')} - </FormLabel> - <div className="text-muted-foreground"> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - </div> - </div> - <div className="flex pt-1"> - <div className="w-1/4"></div> - <FormMessage /> - </div> - </FormItem> - )} - />{' '} - <FormField - control={form.control} - name={'useRAPTORToEnhanceRetrieval'} - render={({ field }) => ( - <FormItem className=" items-center space-y-0 "> - <div className=""> - <FormLabel - tooltip={t('useRAPTORToEnhanceRetrievalTip')} - className="text-sm whitespace-wrap " - > - {t('useRAPTORToEnhanceRetrieval')} - </FormLabel> - <div className="text-muted-foreground"> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - </div> - </div> - <div className="flex pt-1"> - <div className="w-1/4"></div> - <FormMessage /> - </div> - </FormItem> - )} - /> - </> - ); -} - -export function TeamItem() { - const { t } = useTranslate('knowledgeConfiguration'); - const form = useFormContext(); - - return ( - <FormField - control={form.control} - name={'team'} - render={({ field }) => ( - <FormItem className=" items-center space-y-0 "> - <div className=""> - <FormLabel - tooltip={t('teamTip')} - className="text-sm whitespace-wrap " - > - <span className="text-destructive mr-1"> *</span> - {t('team')} - </FormLabel> - <div className="text-muted-foreground"> - <FormControl> - <RAGFlowSelect - {...field} - placeholder={t('teamPlaceholder')} - options={[{ value: '0', label: t('teamDefault') }]} - /> - </FormControl> - </div> - </div> - <div className="flex pt-1"> - <div className="w-1/4"></div> - <FormMessage /> - </div> - </FormItem> - )} - /> - ); -} - export function EnableAutoGenerateItem() { const { t } = useTranslate('knowledgeConfiguration'); const form = useFormContext(); diff --git a/web/src/pages/dataset/dataset-setting/form-schema.ts b/web/src/pages/dataset/dataset-setting/form-schema.ts index 1594ba362..3f1011121 100644 --- a/web/src/pages/dataset/dataset-setting/form-schema.ts +++ b/web/src/pages/dataset/dataset-setting/form-schema.ts @@ -11,6 +11,9 @@ export const formSchema = z.object({ avatar: z.any().nullish(), permission: z.string().optional(), parser_id: z.string(), + pipeline_id: z.string().optional(), + pipeline_name: z.string().optional(), + pipeline_avatar: z.string().optional(), embd_id: z.string(), parser_config: z .object({ @@ -71,3 +74,18 @@ export const formSchema = z.object({ pagerank: z.number(), // icon: z.array(z.instanceof(File)), }); + +export const pipelineFormSchema = z.object({ + pipeline_id: z.string().optional(), + set_default: z.boolean().optional(), + file_filter: z.string().optional(), +}); + +// export const linkPiplineFormSchema = pipelineFormSchema.pick({ +// pipeline_id: true, +// file_filter: true, +// }); +// export const editPiplineFormSchema = pipelineFormSchema.pick({ +// set_default: true, +// file_filter: true, +// }); diff --git a/web/src/pages/dataset/dataset-setting/general-form.tsx b/web/src/pages/dataset/dataset-setting/general-form.tsx index f7dc59e8b..b4a7b9635 100644 --- a/web/src/pages/dataset/dataset-setting/general-form.tsx +++ b/web/src/pages/dataset/dataset-setting/general-form.tsx @@ -1,4 +1,5 @@ import { AvatarUpload } from '@/components/avatar-upload'; +import PageRankFormField from '@/components/page-rank-form-field'; import { FormControl, FormField, @@ -9,6 +10,7 @@ import { import { Input } from '@/components/ui/input'; import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { TagItems } from './components/tag-item'; import { EmbeddingModelItem } from './configuration/common-item'; import { PermissionFormField } from './permission-form-field'; @@ -17,14 +19,14 @@ export function GeneralForm() { const { t } = useTranslation(); return ( - <section className="space-y-4"> + <> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem className="items-center space-y-0"> <div className="flex"> - <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> + <FormLabel className="text-sm whitespace-nowrap w-1/4"> <span className="text-red-600">*</span> {t('common.name')} </FormLabel> @@ -45,7 +47,7 @@ export function GeneralForm() { render={({ field }) => ( <FormItem className="items-center space-y-0"> <div className="flex"> - <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> + <FormLabel className="text-sm whitespace-nowrap w-1/4"> {t('setting.avatar')} </FormLabel> <FormControl className="w-3/4"> @@ -70,7 +72,7 @@ export function GeneralForm() { return ( <FormItem className="items-center space-y-0"> <div className="flex"> - <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> + <FormLabel className="text-sm whitespace-nowrap w-1/4"> {t('flow.description')} </FormLabel> <FormControl className="w-3/4"> @@ -87,6 +89,9 @@ export function GeneralForm() { /> <PermissionFormField></PermissionFormField> <EmbeddingModelItem></EmbeddingModelItem> - </section> + <PageRankFormField></PageRankFormField> + + <TagItems></TagItems> + </> ); } diff --git a/web/src/pages/dataset/dataset-setting/hooks.ts b/web/src/pages/dataset/dataset-setting/hooks.ts index 3332ca3f5..bb9fd8d83 100644 --- a/web/src/pages/dataset/dataset-setting/hooks.ts +++ b/web/src/pages/dataset/dataset-setting/hooks.ts @@ -25,8 +25,10 @@ export function useSelectEmbeddingModelOptions() { return allOptions[LlmModelType.Embedding]; } -export function useHasParsedDocument() { - const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration(); +export function useHasParsedDocument(isEdit?: boolean) { + const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration({ + isEdit, + }); return knowledgeDetails.chunk_num > 0; } @@ -39,6 +41,16 @@ export const useFetchKnowledgeConfigurationOnMount = ( const parser_config = { ...form.formState?.defaultValues?.parser_config, ...knowledgeDetails.parser_config, + raptor: { + ...form.formState?.defaultValues?.parser_config?.raptor, + ...knowledgeDetails.parser_config?.raptor, + use_raptor: true, + }, + graphrag: { + ...form.formState?.defaultValues?.parser_config?.graphrag, + ...knowledgeDetails.parser_config?.graphrag, + use_graphrag: true, + }, }; const formValues = { ...pick({ ...knowledgeDetails, parser_config: parser_config }, [ @@ -52,7 +64,7 @@ export const useFetchKnowledgeConfigurationOnMount = ( 'pagerank', 'avatar', ]), - }; + } as z.infer<typeof formSchema>; form.reset(formValues); }, [form, knowledgeDetails]); diff --git a/web/src/pages/dataset/dataset-setting/index.tsx b/web/src/pages/dataset/dataset-setting/index.tsx index 6c57f40e9..c13fcb6b1 100644 --- a/web/src/pages/dataset/dataset-setting/index.tsx +++ b/web/src/pages/dataset/dataset-setting/index.tsx @@ -1,16 +1,29 @@ +import { IDataPipelineSelectNode } from '@/components/data-pipeline-select'; +import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields'; +import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields'; +import { Button } from '@/components/ui/button'; +import Divider from '@/components/ui/divider'; import { Form } from '@/components/ui/form'; import { DocumentParserType } from '@/constants/knowledge'; import { PermissionRole } from '@/constants/permission'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { TopTitle } from '../dataset-title'; -import { ChunkMethodForm } from './chunk-method-form'; +import { + GenerateType, + IGenerateLogButtonProps, +} from '../dataset/generate-button/generate'; +import LinkDataPipeline, { + IDataPipelineNodeProps, +} from './components/link-data-pipeline'; +import { MainContainer } from './configuration-form-container'; import { formSchema } from './form-schema'; import { GeneralForm } from './general-form'; import { useFetchKnowledgeConfigurationOnMount } from './hooks'; - +import { SavingButton } from './saving-button'; const enum DocumentType { DeepDOC = 'DeepDOC', PlainText = 'Plain Text', @@ -46,24 +59,84 @@ export default function DatasetSettings() { html4excel: false, topn_tags: 3, raptor: { - use_raptor: false, + use_raptor: true, + max_token: 256, + threshold: 0.1, + max_cluster: 64, + random_seed: 0, + prompt: t('knowledgeConfiguration.promptText'), }, graphrag: { - use_graphrag: false, + use_graphrag: true, entity_types: initialEntityTypes, method: MethodValue.Light, }, }, + pipeline_id: '', pagerank: 0, }, }); - useFetchKnowledgeConfigurationOnMount(form); + const knowledgeDetails = useFetchKnowledgeConfigurationOnMount(form); + + const [pipelineData, setPipelineData] = useState<IDataPipelineNodeProps>(); + const [graphRagGenerateData, setGraphRagGenerateData] = + useState<IGenerateLogButtonProps>(); + const [raptorGenerateData, setRaptorGenerateData] = + useState<IGenerateLogButtonProps>(); + useEffect(() => { + console.log('🚀 ~ DatasetSettings ~ knowledgeDetails:', knowledgeDetails); + if (knowledgeDetails) { + const data: IDataPipelineNodeProps = { + id: knowledgeDetails.pipeline_id, + name: knowledgeDetails.pipeline_name, + avatar: knowledgeDetails.pipeline_avatar, + linked: true, + }; + setPipelineData(data); + setGraphRagGenerateData({ + finish_at: knowledgeDetails.mindmap_task_finish_at, + task_id: knowledgeDetails.mindmap_task_id, + } as IGenerateLogButtonProps); + setRaptorGenerateData({ + finish_at: knowledgeDetails.raptor_task_finish_at, + task_id: knowledgeDetails.raptor_task_id, + } as IGenerateLogButtonProps); + } + }, [knowledgeDetails]); async function onSubmit(data: z.infer<typeof formSchema>) { - console.log('🚀 ~ DatasetSettings ~ data:', data); + try { + console.log('Form validation passed, submit data', data); + } catch (error) { + console.error('An error occurred during submission:', error); + } } + const handleLinkOrEditSubmit = ( + data: IDataPipelineSelectNode | undefined, + ) => { + console.log('🚀 ~ DatasetSettings ~ data:', data); + if (data) { + setPipelineData(data); + form.setValue('pipeline_id', data.id || ''); + // form.setValue('pipeline_name', data.name || ''); + // form.setValue('pipeline_avatar', data.avatar || ''); + } + }; + const handleDeletePipelineTask = (type: GenerateType) => { + if (type === GenerateType.KnowledgeGraph) { + setGraphRagGenerateData({ + finish_at: '', + task_id: '', + } as IGenerateLogButtonProps); + } else if (type === GenerateType.Raptor) { + setRaptorGenerateData({ + finish_at: '', + task_id: '', + } as IGenerateLogButtonProps); + } + }; return ( <section className="p-5 h-full flex flex-col"> <TopTitle @@ -76,9 +149,41 @@ export default function DatasetSettings() { onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 flex-1" > - <div className="w-[768px]"> - <GeneralForm></GeneralForm> - <ChunkMethodForm></ChunkMethodForm> + <div className="w-[768px] h-[calc(100vh-240px)] pr-1 overflow-y-auto scrollbar-auto"> + <MainContainer className="text-text-secondary"> + <GeneralForm></GeneralForm> + <Divider /> + + <GraphRagItems + className="border-none p-0" + data={graphRagGenerateData as IGenerateLogButtonProps} + onDelete={() => + handleDeletePipelineTask(GenerateType.KnowledgeGraph) + } + ></GraphRagItems> + <Divider /> + <RaptorFormFields + data={raptorGenerateData as IGenerateLogButtonProps} + onDelete={() => handleDeletePipelineTask(GenerateType.Raptor)} + ></RaptorFormFields> + <Divider /> + <LinkDataPipeline + data={pipelineData} + handleLinkOrEditSubmit={handleLinkOrEditSubmit} + /> + </MainContainer> + </div> + <div className="text-right items-center flex justify-end gap-3 w-[768px]"> + <Button + type="reset" + className="bg-transparent text-color-white hover:bg-transparent border-gray-500 border-[1px]" + onClick={() => { + form.reset(); + }} + > + {t('knowledgeConfiguration.cancel')} + </Button> + <SavingButton></SavingButton> </div> </form> </Form> diff --git a/web/src/pages/dataset/dataset-setting/naive.tsx b/web/src/pages/dataset/dataset-setting/naive.tsx deleted file mode 100644 index 0c4c538bb..000000000 --- a/web/src/pages/dataset/dataset-setting/naive.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields'; -import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields'; -import { - ConfigurationFormContainer, - MainContainer, -} from './configuration-form-container'; -import { EnableAutoGenerateItem } from './configuration/common-item'; - -export function NaiveConfiguration() { - return ( - <MainContainer> - <GraphRagItems className="border-none p-0"></GraphRagItems> - <ConfigurationFormContainer> - <RaptorFormFields></RaptorFormFields> - </ConfigurationFormContainer> - <EnableAutoGenerateItem /> - {/* <ConfigurationFormContainer> - <ChunkMethodItem></ChunkMethodItem> - <LayoutRecognizeFormField></LayoutRecognizeFormField> - - <MaxTokenNumberFormField initialValue={512}></MaxTokenNumberFormField> - <DelimiterFormField></DelimiterFormField> - </ConfigurationFormContainer> - <ConfigurationFormContainer> - <PageRankFormField></PageRankFormField> - <AutoKeywordsFormField></AutoKeywordsFormField> - <AutoQuestionsFormField></AutoQuestionsFormField> - <ExcelToHtmlFormField></ExcelToHtmlFormField> - <TagItems></TagItems> - </ConfigurationFormContainer> */} - </MainContainer> - ); -} diff --git a/web/src/pages/dataset/dataset-setting/saving-button.tsx b/web/src/pages/dataset/dataset-setting/saving-button.tsx index 1fac81ea7..7a063081a 100644 --- a/web/src/pages/dataset/dataset-setting/saving-button.tsx +++ b/web/src/pages/dataset/dataset-setting/saving-button.tsx @@ -62,7 +62,7 @@ export function SavingButton() { if (beValid) { form.handleSubmit(async (values) => { console.log('saveKnowledgeConfiguration: ', values); - delete values['avatar']; + // delete values['avatar']; await saveKnowledgeConfiguration({ kb_id, ...values, diff --git a/web/src/pages/dataset/dataset/constant.ts b/web/src/pages/dataset/dataset/constant.ts index ad6d8f1ae..b4df26100 100644 --- a/web/src/pages/dataset/dataset/constant.ts +++ b/web/src/pages/dataset/dataset/constant.ts @@ -3,15 +3,21 @@ import { RunningStatus } from '@/constants/knowledge'; export const RunningStatusMap = { [RunningStatus.UNSTART]: { label: 'UNSTART', - color: 'var(--accent-primary)', + color: 'rgba(var(--accent-primary))', }, [RunningStatus.RUNNING]: { label: 'Parsing', color: 'var(--team-member)', }, - [RunningStatus.CANCEL]: { label: 'CANCEL', color: 'var(--state-warning)' }, - [RunningStatus.DONE]: { label: 'SUCCESS', color: 'var(--state-success)' }, - [RunningStatus.FAIL]: { label: 'FAIL', color: 'var(--state-error' }, + [RunningStatus.CANCEL]: { + label: 'CANCEL', + color: 'rgba(var(--state-warning))', + }, + [RunningStatus.DONE]: { + label: 'SUCCESS', + color: 'rgba(var(--state-success))', + }, + [RunningStatus.FAIL]: { label: 'FAIL', color: 'rgba(var(--state-error))' }, }; export * from '@/constants/knowledge'; diff --git a/web/src/pages/dataset/dataset/dataset-table.tsx b/web/src/pages/dataset/dataset/dataset-table.tsx index 2182ec2d6..cdbeaa3d3 100644 --- a/web/src/pages/dataset/dataset/dataset-table.tsx +++ b/web/src/pages/dataset/dataset/dataset-table.tsx @@ -27,8 +27,11 @@ import { import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; import { useFetchDocumentList } from '@/hooks/use-document-request'; import { getExtension } from '@/utils/document-util'; +import { t } from 'i18next'; import { pick } from 'lodash'; import { useMemo } from 'react'; +import ProcessLogModal from '../process-log-modal'; +import { useShowLog } from './hooks'; import { SetMetaDialog } from './set-meta-dialog'; import { useChangeDocumentParser } from './use-change-document-parser'; import { useDatasetTableColumns } from './use-dataset-table-columns'; @@ -81,11 +84,13 @@ export function DatasetTable({ onSetMetaModalOk, metaRecord, } = useSaveMeta(); + const { showLog, logInfo, logVisible, hideLog } = useShowLog(documents); const columns = useDatasetTableColumns({ showChangeParserModal, showRenameModal, showSetMetaModal, + showLog, }); const currentPagination = useMemo(() => { @@ -180,6 +185,7 @@ export function DatasetTable({ <ChunkMethodDialog documentId={changeParserRecord.id} parserId={changeParserRecord.parser_id} + pipelineId={changeParserRecord.pipeline_id} parserConfig={changeParserRecord.parser_config} documentExtension={getExtension(changeParserRecord.name)} onOk={onChangeParserOk} @@ -207,6 +213,14 @@ export function DatasetTable({ initialMetaData={metaRecord.meta_fields} ></SetMetaDialog> )} + {logVisible && ( + <ProcessLogModal + title={t('knowledgeDetails.fileLogs')} + visible={logVisible} + onCancel={() => hideLog()} + logInfo={logInfo} + /> + )} </div> ); } diff --git a/web/src/pages/dataset/dataset/generate-button/generate.tsx b/web/src/pages/dataset/dataset/generate-button/generate.tsx new file mode 100644 index 000000000..b9d16e016 --- /dev/null +++ b/web/src/pages/dataset/dataset/generate-button/generate.tsx @@ -0,0 +1,339 @@ +import { IconFontFill } from '@/components/icon-font'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Modal } from '@/components/ui/modal/modal'; +import { cn } from '@/lib/utils'; +import { toFixed } from '@/utils/common-util'; +import { formatDate } from '@/utils/date'; +import { UseMutateAsyncFunction } from '@tanstack/react-query'; +import { t } from 'i18next'; +import { lowerFirst } from 'lodash'; +import { CirclePause, Trash2, WandSparkles } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ProcessingType } from '../../dataset-overview/dataset-common'; +import { replaceText } from '../../process-log-modal'; +import { + ITraceInfo, + generateStatus, + useDatasetGenerate, + useTraceGenerate, + useUnBindTask, +} from './hook'; +export enum GenerateType { + KnowledgeGraph = 'KnowledgeGraph', + Raptor = 'Raptor', +} +export const GenerateTypeMap = { + [GenerateType.KnowledgeGraph]: ProcessingType.knowledgeGraph, + [GenerateType.Raptor]: ProcessingType.raptor, +}; +const MenuItem: React.FC<{ + name: GenerateType; + data: ITraceInfo; + pauseGenerate: ({ + task_id, + type, + }: { + task_id: string; + type: GenerateType; + }) => void; + runGenerate: UseMutateAsyncFunction< + any, + Error, + { + type: GenerateType; + }, + unknown + >; +}> = ({ name: type, runGenerate, data, pauseGenerate }) => { + const iconKeyMap = { + KnowledgeGraph: 'knowledgegraph', + Raptor: 'dataflow-01', + }; + const status = useMemo(() => { + if (!data) { + return generateStatus.start; + } + if (data.progress >= 1) { + return generateStatus.completed; + } else if (!data.progress && data.progress !== 0) { + return generateStatus.start; + } else if (data.progress < 0) { + return generateStatus.failed; + } else if (data.progress < 1) { + return generateStatus.running; + } + }, [data]); + + const percent = + status === generateStatus.failed + ? 100 + : status === generateStatus.running + ? data.progress * 100 + : 0; + + return ( + <DropdownMenuItem + className={cn( + 'border cursor-pointer p-2 rounded-md focus:bg-transparent', + { + 'hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]': + status === generateStatus.start || + status === generateStatus.completed, + 'hover:border-border hover:bg-[rgba(59,160,92,0)]': + status !== generateStatus.start && + status !== generateStatus.completed, + }, + )} + onSelect={(e) => { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + <div + className="flex items-start gap-2 flex-col w-full" + onClick={() => { + if ( + status === generateStatus.start || + status === generateStatus.completed + ) { + runGenerate({ type }); + } + }} + > + <div className="flex justify-start text-text-primary items-center gap-2"> + <IconFontFill + name={iconKeyMap[type]} + className="text-accent-primary" + /> + {t(`knowledgeDetails.${lowerFirst(type)}`)} + </div> + {status === generateStatus.start && ( + <div className="text-text-secondary text-sm"> + {t(`knowledgeDetails.generate${type}`)} + </div> + )} + {(status === generateStatus.running || + status === generateStatus.failed) && ( + <div className="flex justify-between items-center w-full px-2.5 py-1"> + <div + className={cn(' bg-border-button h-1 rounded-full', { + 'w-[calc(100%-100px)]': status === generateStatus.running, + 'w-[calc(100%-50px)]': status === generateStatus.failed, + })} + > + <div + className={cn('h-1 rounded-full', { + 'bg-state-error': status === generateStatus.failed, + 'bg-accent-primary': status === generateStatus.running, + })} + style={{ width: `${toFixed(percent)}%` }} + ></div> + </div> + {status === generateStatus.running && ( + <span>{(toFixed(percent) as string) + '%'}</span> + )} + {status === generateStatus.failed && ( + <span + className="text-state-error" + onClick={(e) => { + e.stopPropagation(); + runGenerate({ type }); + }} + > + <IconFontFill name="reparse" className="text-accent-primary" /> + </span> + )} + {status !== generateStatus.failed && ( + <span + className="text-state-error" + onClick={(e) => { + e.stopPropagation(); + pauseGenerate({ task_id: data.id, type }); + }} + > + <CirclePause /> + </span> + )} + </div> + )} + <div className="w-full whitespace-pre-line text-wrap rounded-lg h-fit max-h-[350px] overflow-y-auto scrollbar-auto px-2.5 py-1"> + {replaceText(data?.progress_msg || '')} + </div> + </div> + </DropdownMenuItem> + ); +}; + +const Generate: React.FC = () => { + const [open, setOpen] = useState(false); + const { graphRunData, raptorRunData } = useTraceGenerate({ open }); + const { runGenerate, pauseGenerate } = useDatasetGenerate(); + const handleOpenChange = (isOpen: boolean) => { + setOpen(isOpen); + console.log('Dropdown is now', isOpen ? 'open' : 'closed'); + }; + + return ( + <div className="generate"> + <DropdownMenu open={open} onOpenChange={handleOpenChange}> + <DropdownMenuTrigger asChild> + <Button + variant={'transparent'} + onClick={() => { + handleOpenChange(!open); + }} + > + <WandSparkles className="mr-2" /> + {t('knowledgeDetails.generate')} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-[380px] p-5 flex flex-col gap-2 "> + {Object.values(GenerateType).map((name) => { + const data = ( + name === GenerateType.KnowledgeGraph + ? graphRunData + : raptorRunData + ) as ITraceInfo; + console.log( + name, + 'data', + data, + !data || (!data.progress && data.progress !== 0), + ); + return ( + <div key={name}> + <MenuItem + name={name} + runGenerate={runGenerate} + data={data} + pauseGenerate={pauseGenerate} + /> + </div> + ); + })} + </DropdownMenuContent> + </DropdownMenu> + </div> + ); +}; + +export default Generate; + +export type IGenerateLogButtonProps = { + finish_at: string; + task_id: string; +}; + +export type IGenerateLogProps = IGenerateLogButtonProps & { + id?: string; + status: 0 | 1; + message?: string; + created_at?: string; + updated_at?: string; + type?: GenerateType; + className?: string; + onDelete?: () => void; +}; +export const GenerateLogButton = (props: IGenerateLogProps) => { + const { t } = useTranslation(); + const { message, finish_at, type, onDelete } = props; + + const { handleUnbindTask } = useUnBindTask(); + + const handleDeleteFunc = async () => { + const data = await handleUnbindTask({ + type: GenerateTypeMap[type as GenerateType], + }); + Modal.destroy(); + console.log('handleUnbindTask', data); + if (data.code === 0) { + onDelete?.(); + } + }; + + const handleDelete = () => { + Modal.show({ + visible: true, + className: '!w-[560px]', + title: + t('common.delete') + + ' ' + + (type === GenerateType.KnowledgeGraph + ? t('knowledgeDetails.knowledgeGraph') + : t('knowledgeDetails.raptor')), + children: ( + <div + className="text-sm text-text-secondary" + dangerouslySetInnerHTML={{ + __html: t('knowledgeConfiguration.deleteGenerateModalContent', { + type: + type === GenerateType.KnowledgeGraph + ? t('knowledgeDetails.knowledgeGraph') + : t('knowledgeDetails.raptor'), + }), + }} + ></div> + ), + onVisibleChange: () => { + Modal.destroy(); + }, + footer: ( + <div className="flex justify-end gap-2"> + <Button + type="button" + variant={'outline'} + onClick={() => Modal.destroy()} + > + {t('dataflowParser.changeStepModalCancelText')} + </Button> + <Button + type="button" + variant={'secondary'} + className="!bg-state-error text-text-primary" + onClick={() => { + handleDeleteFunc(); + }} + > + {t('common.delete')} + </Button> + </div> + ), + }); + }; + + return ( + <div + className={cn('flex bg-bg-card rounded-md py-1 px-3', props.className)} + > + <div className="flex items-center justify-between w-full"> + {finish_at && ( + <> + <div> + {message || t('knowledgeDetails.generatedOn')} + {formatDate(finish_at)} + </div> + <Trash2 + size={14} + className="cursor-pointer" + onClick={(e) => { + console.log('delete'); + handleDelete(); + e.stopPropagation(); + }} + /> + </> + )} + {!finish_at && <div>{t('knowledgeDetails.notGenerated')}</div>} + </div> + </div> + ); +}; diff --git a/web/src/pages/dataset/dataset/generate-button/hook.ts b/web/src/pages/dataset/dataset/generate-button/hook.ts new file mode 100644 index 000000000..772c36ed7 --- /dev/null +++ b/web/src/pages/dataset/dataset/generate-button/hook.ts @@ -0,0 +1,174 @@ +import message from '@/components/ui/message'; +import agentService from '@/services/agent-service'; +import kbService, { deletePipelineTask } from '@/services/knowledge-service'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { t } from 'i18next'; +import { useEffect, useState } from 'react'; +import { useParams } from 'umi'; +import { ProcessingType } from '../../dataset-overview/dataset-common'; +import { GenerateType } from './generate'; +export const generateStatus = { + running: 'running', + completed: 'completed', + start: 'start', + failed: 'failed', +}; + +enum DatasetKey { + generate = 'generate', + pauseGenerate = 'pauseGenerate', +} + +export interface ITraceInfo { + begin_at: string; + chunk_ids: string; + create_date: string; + create_time: number; + digest: string; + doc_id: string; + from_page: number; + id: string; + priority: number; + process_duration: number; + progress: number; + progress_msg: string; + retry_count: number; + task_type: string; + to_page: number; + update_date: string; + update_time: number; +} + +export const useTraceGenerate = ({ open }: { open: boolean }) => { + const { id } = useParams(); + const [isLoopGraphRun, setLoopGraphRun] = useState(false); + const [isLoopRaptorRun, setLoopRaptorRun] = useState(false); + const { data: graphRunData, isFetching: graphRunloading } = + useQuery<ITraceInfo>({ + queryKey: [GenerateType.KnowledgeGraph, id, open], + // initialData: {}, + gcTime: 0, + refetchInterval: isLoopGraphRun ? 5000 : false, + retry: 3, + retryDelay: 1000, + enabled: open, + queryFn: async () => { + const { data } = await kbService.traceGraphRag({ + kb_id: id, + }); + return data?.data || {}; + }, + }); + + const { data: raptorRunData, isFetching: raptorRunloading } = + useQuery<ITraceInfo>({ + queryKey: [GenerateType.Raptor, id, open], + // initialData: {}, + gcTime: 0, + refetchInterval: isLoopRaptorRun ? 5000 : false, + retry: 3, + retryDelay: 1000, + enabled: open, + queryFn: async () => { + const { data } = await kbService.traceRaptor({ + kb_id: id, + }); + return data?.data || {}; + }, + }); + + useEffect(() => { + setLoopGraphRun( + !!( + (graphRunData?.progress || graphRunData?.progress === 0) && + graphRunData?.progress < 1 && + graphRunData?.progress >= 0 + ), + ); + }, [graphRunData?.progress]); + + useEffect(() => { + setLoopRaptorRun( + !!( + (raptorRunData?.progress || raptorRunData?.progress === 0) && + raptorRunData?.progress < 1 && + raptorRunData?.progress >= 0 + ), + ); + }, [raptorRunData?.progress]); + return { + graphRunData, + graphRunloading, + raptorRunData, + raptorRunloading, + }; +}; +export const useDatasetGenerate = () => { + const queryClient = useQueryClient(); + const { id } = useParams(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [DatasetKey.generate], + mutationFn: async ({ type }: { type: GenerateType }) => { + const func = + type === GenerateType.KnowledgeGraph + ? kbService.runGraphRag + : kbService.runRaptor; + const { data } = await func({ + kb_id: id, + }); + if (data.code === 0) { + message.success(t('message.operated')); + queryClient.invalidateQueries({ + queryKey: [type], + }); + } + return data; + }, + }); + // const pauseGenerate = useCallback(() => { + // // TODO: pause generate + // console.log('pause generate'); + // }, []); + const { mutateAsync: pauseGenerate } = useMutation({ + mutationKey: [DatasetKey.pauseGenerate], + mutationFn: async ({ + task_id, + type, + }: { + task_id: string; + type: GenerateType; + }) => { + const { data } = await agentService.cancelDataflow(task_id); + if (data.code === 0) { + message.success(t('message.operated')); + queryClient.invalidateQueries({ + queryKey: [type], + }); + } + return data; + }, + }); + return { runGenerate: mutateAsync, pauseGenerate, data, loading }; +}; + +export const useUnBindTask = () => { + const { id } = useParams(); + const { mutateAsync: handleUnbindTask } = useMutation({ + mutationKey: [DatasetKey.pauseGenerate], + mutationFn: async ({ type }: { type: ProcessingType }) => { + const { data } = await deletePipelineTask({ kb_id: id as string, type }); + if (data.code === 0) { + message.success(t('message.operated')); + // queryClient.invalidateQueries({ + // queryKey: [type], + // }); + } + return data; + }, + }); + return { handleUnbindTask }; +}; diff --git a/web/src/pages/dataset/dataset/generate.tsx b/web/src/pages/dataset/dataset/generate.tsx deleted file mode 100644 index 6dad4d56f..000000000 --- a/web/src/pages/dataset/dataset/generate.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import SvgIcon from '@/components/svg-icon'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { t } from 'i18next'; -import { lowerFirst, toLower } from 'lodash'; -import { WandSparkles } from 'lucide-react'; - -const MenuItem: React.FC<{ name: 'KnowledgeGraph' | 'Raptor' }> = ({ - name, -}) => { - console.log(name, 'pppp'); - return ( - <div className="flex items-start gap-2 flex-col"> - <div className="flex justify-start text-text-primary items-center gap-2"> - <SvgIcon name={`data-flow/${toLower(name)}`} width={24}></SvgIcon> - {t(`knowledgeDetails.${lowerFirst(name)}`)} - </div> - <div className="text-text-secondary text-sm"> - {t(`knowledgeDetails.generate${name}`)} - </div> - </div> - ); -}; - -const Generate: React.FC = () => { - return ( - <div className="generate"> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant={'transparent'}> - <WandSparkles className="mr-2" /> - {t('knowledgeDetails.generate')} - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-[380px] p-2 "> - <DropdownMenuItem className="border cursor-pointer p-2 rounded-md hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]"> - <MenuItem name="KnowledgeGraph" /> - </DropdownMenuItem> - <DropdownMenuItem - className="border cursor-pointer p-2 rounded-md mt-3 hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]" - onSelect={(e) => { - e.preventDefault(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - > - <MenuItem name="Raptor" /> - {/* <div className="flex items-start gap-2 flex-col"> - <div className="flex items-center gap-2"> - <SvgIcon name={`data-flow/raptor`} width={24}></SvgIcon> - {t('knowledgeDetails.raptor')} - </div> - <div>{t('knowledgeDetails.generateRaptor')}</div> - </div> */} - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - </div> - ); -}; - -export default Generate; diff --git a/web/src/pages/dataset/dataset/hooks.ts b/web/src/pages/dataset/dataset/hooks.ts index 1613f9cc9..68d3ef98b 100644 --- a/web/src/pages/dataset/dataset/hooks.ts +++ b/web/src/pages/dataset/dataset/hooks.ts @@ -1,8 +1,13 @@ import { useSetModalState } from '@/hooks/common-hooks'; import { useNextWebCrawl } from '@/hooks/document-hooks'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; -import { useCallback, useState } from 'react'; +import { IDocumentInfo } from '@/interfaces/database/document'; +import { formatDate, formatSecondsToHumanReadable } from '@/utils/date'; +import { formatBytes } from '@/utils/file-util'; +import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'umi'; +import { ILogInfo } from '../process-log-modal'; +import { RunningStatus } from './constant'; export const useNavigateToOtherPage = () => { const navigate = useNavigate(); @@ -58,3 +63,43 @@ export const useHandleWebCrawl = () => { showWebCrawlUploadModal, }; }; + +export const useShowLog = (documents: IDocumentInfo[]) => { + const { showModal, hideModal, visible } = useSetModalState(); + const [record, setRecord] = useState<IDocumentInfo>(); + const logInfo = useMemo(() => { + const findRecord = documents.find( + (item: IDocumentInfo) => item.id === record?.id, + ); + let log: ILogInfo = { + taskId: record?.id, + fileName: record?.name || '-', + details: record?.progress_msg || '-', + }; + if (findRecord) { + log = { + fileType: findRecord?.suffix, + uploadedBy: findRecord?.nickname, + fileName: findRecord?.name, + uploadDate: formatDate(findRecord.create_date), + fileSize: formatBytes(findRecord.size || 0), + processBeginAt: formatDate(findRecord.process_begin_at), + chunkNumber: findRecord.chunk_num, + duration: formatSecondsToHumanReadable( + findRecord.process_duration || 0, + ), + status: findRecord.run as RunningStatus, + details: findRecord.progress_msg, + }; + } + return log; + }, [record, documents]); + const showLog = useCallback( + (data: IDocumentInfo) => { + setRecord(data); + showModal(); + }, + [showModal], + ); + return { showLog, hideLog: hideModal, logVisible: visible, logInfo }; +}; diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx index f7752c5c3..7994d6be8 100644 --- a/web/src/pages/dataset/dataset/index.tsx +++ b/web/src/pages/dataset/dataset/index.tsx @@ -15,7 +15,7 @@ import { useFetchDocumentList } from '@/hooks/use-document-request'; import { Upload } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { DatasetTable } from './dataset-table'; -import Generate from './generate'; +import Generate from './generate-button/generate'; import { useBulkOperateDataset } from './use-bulk-operate-dataset'; import { useCreateEmptyDocument } from './use-create-empty-document'; import { useSelectDatasetFilters } from './use-select-filters'; @@ -75,7 +75,7 @@ export default function Dataset() { filters={filters} leftPanel={ <div className="items-start"> - <div className="pb-1">{t('knowledgeDetails.dataset')}</div> + <div className="pb-1">{t('knowledgeDetails.subbarFiles')}</div> <div className="text-text-sub-title-invert text-sm"> {t('knowledgeDetails.datasetDescription')} </div> diff --git a/web/src/pages/dataset/dataset/parsing-card.tsx b/web/src/pages/dataset/dataset/parsing-card.tsx index d9cdefec3..01f07ecd4 100644 --- a/web/src/pages/dataset/dataset/parsing-card.tsx +++ b/web/src/pages/dataset/dataset/parsing-card.tsx @@ -1,9 +1,4 @@ import { Button } from '@/components/ui/button'; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from '@/components/ui/hover-card'; import { IDocumentInfo } from '@/interfaces/database/document'; import { useTranslation } from 'react-i18next'; import reactStringReplace from 'react-string-replace'; @@ -11,6 +6,7 @@ import { RunningStatus, RunningStatusMap } from './constant'; interface IProps { record: IDocumentInfo; + handleShowLog?: (record: IDocumentInfo) => void; } function Dot({ run }: { run: RunningStatus }) { @@ -85,17 +81,15 @@ export const PopoverContent = ({ record }: IProps) => { ); }; -export function ParsingCard({ record }: IProps) { +export function ParsingCard({ record, handleShowLog }: IProps) { return ( - <HoverCard> - <HoverCardTrigger asChild> - <Button variant={'transparent'} className="border-none" size={'sm'}> - <Dot run={record.run}></Dot> - </Button> - </HoverCardTrigger> - <HoverCardContent className="w-[40vw]"> - <PopoverContent record={record}></PopoverContent> - </HoverCardContent> - </HoverCard> + <Button + variant={'transparent'} + className="border-none" + size={'sm'} + onClick={() => handleShowLog?.(record)} + > + <Dot run={record.run}></Dot> + </Button> ); } diff --git a/web/src/pages/dataset/dataset/parsing-status-cell.tsx b/web/src/pages/dataset/dataset/parsing-status-cell.tsx index 8ada0b09d..b029ceed7 100644 --- a/web/src/pages/dataset/dataset/parsing-status-cell.tsx +++ b/web/src/pages/dataset/dataset/parsing-status-cell.tsx @@ -1,4 +1,5 @@ import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { IconFontFill } from '@/components/icon-font'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -6,41 +7,56 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from '@/components/ui/hover-card'; import { Progress } from '@/components/ui/progress'; import { Separator } from '@/components/ui/separator'; import { IDocumentInfo } from '@/interfaces/database/document'; -import { CircleX, RefreshCw } from 'lucide-react'; +import { CircleX } from 'lucide-react'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { DocumentType, RunningStatus } from './constant'; -import { ParsingCard, PopoverContent } from './parsing-card'; +import { ParsingCard } from './parsing-card'; import { UseChangeDocumentParserShowType } from './use-change-document-parser'; import { useHandleRunDocumentByIds } from './use-run-document'; import { UseSaveMetaShowType } from './use-save-meta'; import { isParserRunning } from './utils'; const IconMap = { [RunningStatus.UNSTART]: ( - <div className="w-0 h-0 border-l-[10px] border-l-accent-primary border-t-8 border-r-4 border-b-8 border-transparent"></div> + <IconFontFill name="play" className="text-accent-primary" /> + ), + [RunningStatus.RUNNING]: ( + <CircleX size={14} color="rgba(var(--state-error))" /> + ), + [RunningStatus.CANCEL]: ( + <IconFontFill name="reparse" className="text-accent-primary" /> + ), + [RunningStatus.DONE]: ( + <IconFontFill name="reparse" className="text-accent-primary" /> + ), + [RunningStatus.FAIL]: ( + <IconFontFill name="reparse" className="text-accent-primary" /> ), - [RunningStatus.RUNNING]: <CircleX size={14} color="var(--state-error)" />, - [RunningStatus.CANCEL]: <RefreshCw size={14} color="var(--accent-primary)" />, - [RunningStatus.DONE]: <RefreshCw size={14} color="var(--accent-primary)" />, - [RunningStatus.FAIL]: <RefreshCw size={14} color="var(--accent-primary)" />, }; export function ParsingStatusCell({ record, showChangeParserModal, showSetMetaModal, -}: { record: IDocumentInfo } & UseChangeDocumentParserShowType & + showLog, +}: { + record: IDocumentInfo; + showLog: (record: IDocumentInfo) => void; +} & UseChangeDocumentParserShowType & UseSaveMetaShowType) { const { t } = useTranslation(); - const { run, parser_id, progress, chunk_num, id } = record; + const { + run, + parser_id, + pipeline_id, + pipeline_name, + progress, + chunk_num, + id, + } = record; const operationIcon = IconMap[run]; const p = Number((progress * 100).toFixed(2)); const { handleRunDocumentByIds } = useHandleRunDocumentByIds(id); @@ -65,18 +81,25 @@ export function ParsingStatusCell({ return record.type !== DocumentType.Virtual; }, [record]); + const handleShowLog = (record: IDocumentInfo) => { + showLog(record); + }; return ( <section className="flex gap-8 items-center"> <div className="w-[100px] text-ellipsis overflow-hidden flex items-center justify-between"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant={'transparent'} className="border-none" size={'sm'}> - {parser_id === 'naive' ? 'general' : parser_id} + {pipeline_id + ? pipeline_name || pipeline_id + : parser_id === 'naive' + ? 'general' + : parser_id} </Button> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem onClick={handleShowChangeParserModal}> - {t('knowledgeDetails.chunkMethod')} + {t('knowledgeDetails.dataPipeline')} </DropdownMenuItem> <DropdownMenuItem onClick={handleShowSetMetaModal}> {t('knowledgeDetails.setMetaData')} @@ -85,41 +108,54 @@ export function ParsingStatusCell({ </DropdownMenu> </div> {showParse && ( - <> - <ConfirmDeleteDialog - title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })} - hidden={isZeroChunk || isRunning} - onOk={handleOperationIconClick(true)} - onCancel={handleOperationIconClick(false)} - > - <div - className="cursor-pointer flex items-center gap-3" - onClick={ - isZeroChunk || isRunning - ? handleOperationIconClick(false) - : () => {} - } + <div className="flex items-center gap-3"> + <Separator orientation="vertical" className="h-2.5" /> + {!isParserRunning(run) && ( + <ConfirmDeleteDialog + title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })} + hidden={isZeroChunk || isRunning} + onOk={handleOperationIconClick(true)} + onCancel={handleOperationIconClick(false)} > - <Separator orientation="vertical" className="h-2.5" /> - {operationIcon} - </div> - </ConfirmDeleteDialog> - {isParserRunning(run) ? ( - <HoverCard> - <HoverCardTrigger asChild> - <div className="flex items-center gap-1"> - <Progress value={p} className="h-1 flex-1 min-w-10" /> - {p}% - </div> - </HoverCardTrigger> - <HoverCardContent className="w-[40vw]"> - <PopoverContent record={record}></PopoverContent> - </HoverCardContent> - </HoverCard> - ) : ( - <ParsingCard record={record}></ParsingCard> + <div + className="cursor-pointer flex items-center gap-3" + onClick={ + isZeroChunk || isRunning + ? handleOperationIconClick(false) + : () => {} + } + > + {!isParserRunning(run) && operationIcon} + </div> + </ConfirmDeleteDialog> )} - </> + {isParserRunning(run) ? ( + <> + <div + className="flex items-center gap-1 cursor-pointer" + onClick={() => handleShowLog(record)} + > + <Progress value={p} className="h-1 flex-1 min-w-10" /> + {p}% + </div> + <div + className="cursor-pointer flex items-center gap-3" + onClick={ + isZeroChunk || isRunning + ? handleOperationIconClick(false) + : () => {} + } + > + {operationIcon} + </div> + </> + ) : ( + <ParsingCard + record={record} + handleShowLog={handleShowLog} + ></ParsingCard> + )} + </div> )} </section> ); diff --git a/web/src/pages/dataset/dataset/use-change-document-parser.ts b/web/src/pages/dataset/dataset/use-change-document-parser.ts index d9898aeac..0457fad84 100644 --- a/web/src/pages/dataset/dataset/use-change-document-parser.ts +++ b/web/src/pages/dataset/dataset/use-change-document-parser.ts @@ -19,6 +19,7 @@ export const useChangeDocumentParser = () => { if (record?.id) { const ret = await setDocumentParser({ parserId: parserConfigInfo.parser_id, + pipelineId: parserConfigInfo.pipeline_id, documentId: record?.id, parserConfig: parserConfigInfo.parser_config, }); diff --git a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx index fc492db13..762264b0c 100644 --- a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx +++ b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx @@ -23,12 +23,13 @@ import { UseSaveMetaShowType } from './use-save-meta'; type UseDatasetTableColumnsType = UseChangeDocumentParserShowType & UseRenameDocumentShowType & - UseSaveMetaShowType; + UseSaveMetaShowType & { showLog: (record: IDocumentInfo) => void }; export function useDatasetTableColumns({ showChangeParserModal, showRenameModal, showSetMetaModal, + showLog, }: UseDatasetTableColumnsType) { const { t } = useTranslation('translation', { keyPrefix: 'knowledgeDetails', @@ -151,6 +152,7 @@ export function useDatasetTableColumns({ record={row.original} showChangeParserModal={showChangeParserModal} showSetMetaModal={showSetMetaModal} + showLog={showLog} ></ParsingStatusCell> ); }, diff --git a/web/src/pages/dataset/process-log-modal.tsx b/web/src/pages/dataset/process-log-modal.tsx new file mode 100644 index 000000000..96b0c0e12 --- /dev/null +++ b/web/src/pages/dataset/process-log-modal.tsx @@ -0,0 +1,155 @@ +import FileStatusBadge from '@/components/file-status-badge'; +import { Button } from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal/modal'; +import { RunningStatusMap } from '@/constants/knowledge'; +import { useTranslate } from '@/hooks/common-hooks'; +import React, { useMemo } from 'react'; +import reactStringReplace from 'react-string-replace'; +import { RunningStatus } from './dataset/constant'; +export interface ILogInfo { + fileType?: string; + uploadedBy?: string; + uploadDate?: string; + processBeginAt?: string; + chunkNumber?: number; + + taskId?: string; + fileName: string; + fileSize?: string; + source?: string; + task?: string; + status?: RunningStatus; + startTime?: string; + endTime?: string; + duration?: string; + details: string; +} + +interface ProcessLogModalProps { + visible: boolean; + onCancel: () => void; + logInfo: ILogInfo; + title: string; +} + +const InfoItem: React.FC<{ + label: string; + value: string | React.ReactNode; + className?: string; +}> = ({ label, value, className = '' }) => { + return ( + <div className={`flex flex-col mb-4 ${className}`}> + <span className="text-text-secondary text-sm">{label}</span> + <span className="text-text-primary mt-1">{value}</span> + </div> + ); +}; +export const replaceText = (text: string) => { + // Remove duplicate \n + const nextText = text.replace(/(\n)\1+/g, '$1'); + + const replacedText = reactStringReplace( + nextText, + /(\[ERROR\].+\s)/g, + (match, i) => { + return ( + <span key={i} className={'text-red-600'}> + {match} + </span> + ); + }, + ); + + return replacedText; +}; +const ProcessLogModal: React.FC<ProcessLogModalProps> = ({ + visible, + onCancel, + logInfo: initData, + title, +}) => { + const { t } = useTranslate('knowledgeDetails'); + const blackKeyList = ['']; + console.log('logInfo', initData); + const logInfo = useMemo(() => { + console.log('logInfo', initData); + return initData; + }, [initData]); + + return ( + <Modal + title={title || 'log'} + open={visible} + onCancel={onCancel} + footer={ + <div className="flex justify-end"> + <Button onClick={onCancel}>{t('close')}</Button> + </div> + } + className="process-log-modal" + > + <div className=" rounded-lg"> + <div className="flex flex-wrap "> + {Object?.keys(logInfo).map((key) => { + if ( + blackKeyList.includes(key) || + !logInfo[key as keyof typeof logInfo] + ) { + return null; + } + if (key === 'details') { + return ( + <div className="w-full mt-2" key={key}> + <InfoItem + label={t(key)} + value={ + <div className="w-full whitespace-pre-line text-wrap bg-bg-card rounded-lg h-fit max-h-[350px] overflow-y-auto scrollbar-auto p-2.5"> + {replaceText(logInfo.details)} + </div> + } + /> + </div> + ); + } + if (key === 'status') { + return ( + <div className="flex flex-col w-1/2" key={key}> + <span className="text-text-secondary text-sm"> + {t('status')} + </span> + <div className="mt-1"> + <FileStatusBadge + status={logInfo.status as RunningStatus} + name={RunningStatusMap[logInfo.status as RunningStatus]} + /> + </div> + </div> + ); + } + return ( + <div className="w-1/2" key={key}> + <InfoItem + label={t(key)} + value={logInfo[key as keyof typeof logInfo]} + /> + </div> + ); + })} + </div> + {/* <InfoItem label="Details" value={logInfo.details} /> */} + {/* <div> + <div>Details</div> + <div> + <ul className="space-y-2"> + <div className={'w-full whitespace-pre-line text-wrap '}> + {replaceText(logInfo.details)} + </div> + </ul> + </div> + </div> */} + </div> + </Modal> + ); +}; + +export default ProcessLogModal; diff --git a/web/src/pages/dataset/sidebar/index.tsx b/web/src/pages/dataset/sidebar/index.tsx index a93414a30..277eb6b83 100644 --- a/web/src/pages/dataset/sidebar/index.tsx +++ b/web/src/pages/dataset/sidebar/index.tsx @@ -9,7 +9,7 @@ import { cn, formatBytes } from '@/lib/utils'; import { Routes } from '@/routes'; import { formatPureDate } from '@/utils/date'; import { isEmpty } from 'lodash'; -import { Banknote, Database, FileSearch2, GitGraph } from 'lucide-react'; +import { Banknote, DatabaseZap, FileSearch2, FolderOpen } from 'lucide-react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useHandleMenuClick } from './hooks'; @@ -22,36 +22,36 @@ export function SideBar({ refreshCount }: PropType) { const pathName = useSecondPathName(); const { handleMenuClick } = useHandleMenuClick(); // refreshCount: be for avatar img sync update on top left - const { data } = useFetchKnowledgeBaseConfiguration(refreshCount); + const { data } = useFetchKnowledgeBaseConfiguration({ refreshCount }); const { data: routerData } = useFetchKnowledgeGraph(); const { t } = useTranslation(); const items = useMemo(() => { const list = [ - // { - // icon: DatabaseZap, - // label: t(`knowledgeDetails.overview`), - // key: Routes.DataSetOverview, - // }, { - icon: Database, - label: t(`knowledgeDetails.dataset`), + icon: <DatabaseZap className="size-4" />, + label: t(`knowledgeDetails.overview`), + key: Routes.DataSetOverview, + }, + { + icon: <FolderOpen className="size-4" />, + label: t(`knowledgeDetails.subbarFiles`), key: Routes.DatasetBase, }, { - icon: FileSearch2, + icon: <FileSearch2 className="size-4" />, label: t(`knowledgeDetails.testing`), key: Routes.DatasetTesting, }, { - icon: Banknote, + icon: <Banknote className="size-4" />, label: t(`knowledgeDetails.configuration`), - key: Routes.DatasetSetting, + key: Routes.DataSetSetting, }, ]; if (!isEmpty(routerData?.graph)) { list.push({ - icon: GitGraph, + icon: <IconFontFill name="knowledgegraph" className="size-4" />, label: t(`knowledgeDetails.knowledgeGraph`), key: Routes.KnowledgeGraph, }); @@ -99,7 +99,7 @@ export function SideBar({ refreshCount }: PropType) { )} onClick={handleMenuClick(item.key)} > - <item.icon className="size-4" /> + {item.icon} <span>{item.label}</span> </Button> ); diff --git a/web/src/pages/datasets/dataset-creating-dialog.tsx b/web/src/pages/datasets/dataset-creating-dialog.tsx index dce755746..c2bbf3705 100644 --- a/web/src/pages/datasets/dataset-creating-dialog.tsx +++ b/web/src/pages/datasets/dataset-creating-dialog.tsx @@ -1,3 +1,4 @@ +import { DataFlowSelect } from '@/components/data-pipeline-select'; import { ButtonLoading } from '@/components/ui/button'; import { Dialog, @@ -15,38 +16,94 @@ import { FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { IModalProps } from '@/interfaces/common'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; +import { useEffect } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; +import { + ChunkMethodItem, + EmbeddingModelItem, + ParseTypeItem, +} from '../dataset/dataset-setting/configuration/common-item'; const FormId = 'dataset-creating-form'; export function InputForm({ onOk }: IModalProps<any>) { const { t } = useTranslation(); - const FormSchema = z.object({ - name: z - .string() - .min(1, { - message: t('knowledgeList.namePlaceholder'), - }) - .trim(), - parseType: z.number().optional(), - }); + const FormSchema = z + .object({ + name: z + .string() + .min(1, { + message: t('knowledgeList.namePlaceholder'), + }) + .trim(), + parseType: z.number().optional(), + embd_id: z + .string() + .min(1, { + message: t('knowledgeConfiguration.embeddingModelPlaceholder'), + }) + .trim(), + parser_id: z.string().optional(), + pipeline_id: z.string().optional(), + }) + .superRefine((data, ctx) => { + // When parseType === 1, parser_id is required + if ( + data.parseType === 1 && + (!data.parser_id || data.parser_id.trim() === '') + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t('knowledgeList.parserRequired'), + path: ['parser_id'], + }); + } + + console.log('form-data', data); + // When parseType === 1, pipline_id required + if (data.parseType === 2 && !data.pipeline_id) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t('knowledgeList.dataFlowRequired'), + path: ['pipeline_id'], + }); + } + }); const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { name: '', parseType: 1, + parser_id: '', + embd_id: '', }, }); function onSubmit(data: z.infer<typeof FormSchema>) { - onOk?.(data.name); + console.log('submit', data); + onOk?.(data); } + + const parseType = useWatch({ + control: form.control, + name: 'parseType', + }); + + useEffect(() => { + console.log('parseType', parseType); + if (parseType === 1) { + form.setValue('pipeline_id', ''); + } + }, [parseType, form]); + const { navigateToAgents } = useNavigatePage(); + return ( <Form {...form}> <form @@ -73,6 +130,17 @@ export function InputForm({ onOk }: IModalProps<any>) { </FormItem> )} /> + + <EmbeddingModelItem line={2} isEdit={false} /> + <ParseTypeItem /> + {parseType === 1 && <ChunkMethodItem></ChunkMethodItem>} + {parseType === 2 && ( + <DataFlowSelect + isMult={false} + toDataPipeline={navigateToAgents} + formFieldName="pipeline_id" + /> + )} </form> </Form> ); @@ -87,7 +155,7 @@ export function DatasetCreatingDialog({ return ( <Dialog open onOpenChange={hideModal}> - <DialogContent className="sm:max-w-[425px]"> + <DialogContent className="sm:max-w-[425px] focus-visible:!outline-none"> <DialogHeader> <DialogTitle>{t('knowledgeList.createKnowledgeBase')}</DialogTitle> </DialogHeader> diff --git a/web/src/pages/datasets/hooks.ts b/web/src/pages/datasets/hooks.ts index 81f9d74f7..d194af9e1 100644 --- a/web/src/pages/datasets/hooks.ts +++ b/web/src/pages/datasets/hooks.ts @@ -2,7 +2,6 @@ import { useSetModalState } from '@/hooks/common-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useCreateKnowledge } from '@/hooks/use-knowledge-request'; import { useCallback, useState } from 'react'; - export const useSearchKnowledge = () => { const [searchString, setSearchString] = useState<string>(''); @@ -15,16 +14,19 @@ export const useSearchKnowledge = () => { }; }; +export interface Iknowledge { + name: string; + embd_id: string; + parser_id: string; +} export const useSaveKnowledge = () => { const { visible: visible, hideModal, showModal } = useSetModalState(); const { loading, createKnowledge } = useCreateKnowledge(); const { navigateToDataset } = useNavigatePage(); const onCreateOk = useCallback( - async (name: string) => { - const ret = await createKnowledge({ - name, - }); + async (data: Iknowledge) => { + const ret = await createKnowledge(data); if (ret?.code === 0) { hideModal(); diff --git a/web/src/pages/datasets/process-log-modal.tsx b/web/src/pages/datasets/process-log-modal.tsx deleted file mode 100644 index 3b2c74b18..000000000 --- a/web/src/pages/datasets/process-log-modal.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { Modal } from '@/components/ui/modal/modal'; -import { useTranslate } from '@/hooks/common-hooks'; -import React from 'react'; - -interface ProcessLogModalProps { - visible: boolean; - onCancel: () => void; - taskInfo: { - taskId: string; - fileName: string; - fileSize: string; - source: string; - task: string; - state: 'Running' | 'Completed' | 'Failed' | 'Pending'; - startTime: string; - endTime?: string; - duration?: string; - details: string; - }; -} - -const StatusTag: React.FC<{ state: string }> = ({ state }) => { - const getTagStyle = () => { - switch (state) { - case 'Running': - return 'bg-green-500 text-green-100'; - case 'Completed': - return 'bg-blue-500 text-blue-100'; - case 'Failed': - return 'bg-red-500 text-red-100'; - case 'Pending': - return 'bg-yellow-500 text-yellow-100'; - default: - return 'bg-gray-500 text-gray-100'; - } - }; - - return ( - <span - className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getTagStyle()}`} - > - <span className="w-1.5 h-1.5 rounded-full mr-1 bg-current"></span> - {state} - </span> - ); -}; - -const InfoItem: React.FC<{ - label: string; - value: string | React.ReactNode; - className?: string; -}> = ({ label, value, className = '' }) => { - return ( - <div className={`flex flex-col mb-4 ${className}`}> - <span className="text-text-secondary text-sm">{label}</span> - <span className="text-text-primary mt-1">{value}</span> - </div> - ); -}; - -const ProcessLogModal: React.FC<ProcessLogModalProps> = ({ - visible, - onCancel, - taskInfo, -}) => { - const { t } = useTranslate('knowledgeDetails'); - return ( - <Modal - title={t('processLog')} - open={visible} - onCancel={onCancel} - footer={ - <div className="flex justify-end"> - <Button onClick={onCancel}>{t('close')}</Button> - </div> - } - className="process-log-modal" - > - <div className="p-6 rounded-lg"> - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> - {/* Left Column */} - <div className="space-y-4"> - <InfoItem label="Task ID" value={taskInfo.taskId} /> - <InfoItem label="File Name" value={taskInfo.fileName} /> - <InfoItem label="File Size" value={taskInfo.fileSize} /> - <InfoItem label="Source" value={taskInfo.source} /> - <InfoItem label="Task" value={taskInfo.task} /> - <InfoItem label="Details" value={taskInfo.details} /> - </div> - - {/* Right Column */} - <div className="space-y-4"> - <div className="flex flex-col"> - <span className="text-text-secondary text-sm">States</span> - <div className="mt-1"> - <StatusTag state={taskInfo.state} /> - </div> - </div> - - <InfoItem label="Start Time" value={taskInfo.startTime} /> - - <InfoItem label="End Time" value={taskInfo.endTime || '-'} /> - - <InfoItem - label="Duration" - value={taskInfo.duration ? `${taskInfo.duration}s` : '-'} - /> - </div> - </div> - </div> - </Modal> - ); -}; - -export default ProcessLogModal; diff --git a/web/src/pages/datasets/use-select-owners.ts b/web/src/pages/datasets/use-select-owners.ts index 50913a023..1de279508 100644 --- a/web/src/pages/datasets/use-select-owners.ts +++ b/web/src/pages/datasets/use-select-owners.ts @@ -1,18 +1,11 @@ import { FilterCollection } from '@/components/list-filter-bar/interface'; -import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; -import { groupListByType } from '@/utils/dataset-util'; -import { useMemo } from 'react'; +import { useFetchKnowledgeList } from '@/hooks/use-knowledge-request'; +import { buildOwnersFilter } from '@/utils/list-filter-util'; export function useSelectOwners() { const { list } = useFetchKnowledgeList(); - const owners = useMemo(() => { - return groupListByType(list, 'tenant_id', 'nickname'); - }, [list]); - - const filters: FilterCollection[] = [ - { field: 'owner', list: owners, label: 'Owner' }, - ]; + const filters: FilterCollection[] = [buildOwnersFilter(list)]; return filters; } diff --git a/web/src/pages/flow/canvas/context-menu/index.less b/web/src/pages/flow/canvas/context-menu/index.less deleted file mode 100644 index 5594aa912..000000000 --- a/web/src/pages/flow/canvas/context-menu/index.less +++ /dev/null @@ -1,18 +0,0 @@ -.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/flow/canvas/context-menu/index.tsx b/web/src/pages/flow/canvas/context-menu/index.tsx deleted file mode 100644 index 6cb306af9..000000000 --- a/web/src/pages/flow/canvas/context-menu/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -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 ( - <div - style={{ top, left, right, bottom }} - className={styles.contextMenu} - {...props} - > - <p style={{ margin: '0.5em' }}> - <small>node: {id}</small> - </p> - <button onClick={duplicateNode} type={'button'}> - duplicate - </button> - <button onClick={deleteNode} type={'button'}> - delete - </button> - </div> - ); -} - -/* @deprecated - */ -export const useHandleNodeContextMenu = (sideWidth: number) => { - const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu); - const ref = useRef<any>(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/flow/canvas/edge/index.less b/web/src/pages/flow/canvas/edge/index.less deleted file mode 100644 index 281b67251..000000000 --- a/web/src/pages/flow/canvas/edge/index.less +++ /dev/null @@ -1,31 +0,0 @@ -.edgeButton { - width: 14px; - height: 14px; - background: #eee; - border: 1px solid #fff; - padding: 0; - cursor: pointer; - border-radius: 50%; - font-size: 10px; - line-height: 1; -} - -.edgeButton:hover { - box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08); -} - -.edgeButtonDark { - width: 14px; - height: 14px; - background: #0e0c0c; - border: 1px solid #fff; - padding: 0; - cursor: pointer; - border-radius: 50%; - font-size: 10px; - line-height: 1; -} - -.edgeButtonDark:hover { - box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08); -} diff --git a/web/src/pages/flow/canvas/edge/index.tsx b/web/src/pages/flow/canvas/edge/index.tsx deleted file mode 100644 index 52f939b8d..000000000 --- a/web/src/pages/flow/canvas/edge/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { - BaseEdge, - EdgeLabelRenderer, - EdgeProps, - getBezierPath, -} from '@xyflow/react'; -import useGraphStore from '../../store'; - -import { useTheme } from '@/components/theme-provider'; -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { useMemo } from 'react'; -import styles from './index.less'; - -export function ButtonEdge({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - source, - target, - style = {}, - markerEnd, - selected, -}: EdgeProps) { - const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById); - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - const { theme } = useTheme(); - const selectedStyle = useMemo(() => { - return selected ? { strokeWidth: 2, stroke: '#1677ff' } : {}; - }, [selected]); - - const onEdgeClick = () => { - deleteEdgeById(id); - }; - - // highlight the nodes that the workflow passes through - const { data: flowDetail } = useFetchFlow(); - - 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 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: 2, stroke: 'red' }; - } - } - return {}; - }, [source, target, graphPath]); - - return ( - <> - <BaseEdge - path={edgePath} - markerEnd={markerEnd} - style={{ ...style, ...selectedStyle, ...highlightStyle }} - /> - <EdgeLabelRenderer> - <div - style={{ - position: 'absolute', - transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, - fontSize: 12, - // everything inside EdgeLabelRenderer has no pointer events by default - // if you have an interactive element, set pointer-events: all - pointerEvents: 'all', - zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 - }} - className="nodrag nopan" - > - <button - className={ - theme === 'dark' ? styles.edgeButtonDark : styles.edgeButton - } - type="button" - onClick={onEdgeClick} - > - × - </button> - </div> - </EdgeLabelRenderer> - </> - ); -} diff --git a/web/src/pages/flow/canvas/index.less b/web/src/pages/flow/canvas/index.less deleted file mode 100644 index d824d88f1..000000000 --- a/web/src/pages/flow/canvas/index.less +++ /dev/null @@ -1,10 +0,0 @@ -.canvasWrapper { - position: relative; - height: 100%; - :global(.react-flow__node-group) { - .commonNode(); - padding: 0; - border: 0; - background-color: transparent; - } -} diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx deleted file mode 100644 index e9ce10ac7..000000000 --- a/web/src/pages/flow/canvas/index.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { - Background, - ConnectionMode, - ControlButton, - Controls, - NodeTypes, - ReactFlow, -} from '@xyflow/react'; -import '@xyflow/react/dist/style.css'; -import { Book, FolderInput, FolderOutput } from 'lucide-react'; -import ChatDrawer from '../chat/drawer'; -import FormDrawer from '../flow-drawer'; -import { - useHandleDrop, - useSelectCanvasData, - useValidateConnection, - useWatchNodeFormDataChange, -} from '../hooks'; -import { useBeforeDelete } from '../hooks/use-before-delete'; -import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json'; -import { useOpenDocument } from '../hooks/use-open-document'; -import { useShowDrawer } from '../hooks/use-show-drawer'; -import JsonUploadModal from '../json-upload-modal'; -import RunDrawer from '../run-drawer'; -import { ButtonEdge } from './edge'; -import styles from './index.less'; -import { RagNode } from './node'; -import { BeginNode } from './node/begin-node'; -import { CategorizeNode } from './node/categorize-node'; -import { EmailNode } from './node/email-node'; -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'; - -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, -}; - -export const edgeTypes = { - buttonEdge: ButtonEdge, -}; - -interface IProps { - drawerVisible: boolean; - hideDrawer(): void; -} - -function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { - const { - nodes, - edges, - onConnect, - onEdgesChange, - onNodesChange, - onSelectionChange, - } = useSelectCanvasData(); - const isValidConnection = useValidateConnection(); - - const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(); - - const { - handleExportJson, - handleImportJson, - fileUploadVisible, - onFileUploadOk, - hideFileUploadModal, - } = useHandleExportOrImportJsonFile(); - - const openDocument = useOpenDocument(); - - const { - onNodeClick, - onPaneClick, - clickedNode, - formDrawerVisible, - hideFormDrawer, - singleDebugDrawerVisible, - hideSingleDebugDrawer, - showSingleDebugDrawer, - chatVisible, - runVisible, - hideRunOrChatDrawer, - showChatModal, - } = useShowDrawer({ - drawerVisible, - hideDrawer, - }); - - const { handleBeforeDelete } = useBeforeDelete(); - - useWatchNodeFormDataChange(); - - return ( - <div className={styles.canvasWrapper}> - <svg - xmlns="http://www.w3.org/2000/svg" - style={{ position: 'absolute', top: 10, left: 0 }} - > - <defs> - <marker - fill="rgb(157 149 225)" - id="logo" - viewBox="0 0 40 40" - refX="8" - refY="5" - markerUnits="strokeWidth" - markerWidth="20" - markerHeight="20" - orient="auto-start-reverse" - > - <path d="M 0 0 L 10 5 L 0 10 z" /> - </marker> - </defs> - </svg> - <ReactFlow - connectionMode={ConnectionMode.Loose} - nodes={nodes} - onNodesChange={onNodesChange} - edges={edges} - onEdgesChange={onEdgesChange} - fitView - onConnect={onConnect} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - onDrop={onDrop} - onDragOver={onDragOver} - onNodeClick={onNodeClick} - onPaneClick={onPaneClick} - onInit={setReactFlowInstance} - onSelectionChange={onSelectionChange} - nodeOrigin={[0.5, 0]} - isValidConnection={isValidConnection} - defaultEdgeOptions={{ - type: 'buttonEdge', - markerEnd: 'logo', - style: { - strokeWidth: 2, - stroke: 'rgb(202 197 245)', - }, - zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 - }} - deleteKeyCode={['Delete', 'Backspace']} - onBeforeDelete={handleBeforeDelete} - > - <Background /> - <Controls className="text-black !flex-col-reverse"> - <ControlButton onClick={handleImportJson}> - <Tooltip> - <TooltipTrigger asChild> - <FolderInput className="!fill-none" /> - </TooltipTrigger> - <TooltipContent>Import</TooltipContent> - </Tooltip> - </ControlButton> - <ControlButton onClick={handleExportJson}> - <Tooltip> - <TooltipTrigger asChild> - <FolderOutput className="!fill-none" /> - </TooltipTrigger> - <TooltipContent>Export</TooltipContent> - </Tooltip> - </ControlButton> - <ControlButton onClick={openDocument}> - <Tooltip> - <TooltipTrigger asChild> - <Book className="!fill-none" /> - </TooltipTrigger> - <TooltipContent>Document</TooltipContent> - </Tooltip> - </ControlButton> - </Controls> - </ReactFlow> - {formDrawerVisible && ( - <FormDrawer - node={clickedNode} - visible={formDrawerVisible} - hideModal={hideFormDrawer} - singleDebugDrawerVisible={singleDebugDrawerVisible} - hideSingleDebugDrawer={hideSingleDebugDrawer} - showSingleDebugDrawer={showSingleDebugDrawer} - ></FormDrawer> - )} - {chatVisible && ( - <ChatDrawer - visible={chatVisible} - hideModal={hideRunOrChatDrawer} - ></ChatDrawer> - )} - - {runVisible && ( - <RunDrawer - hideModal={hideRunOrChatDrawer} - showModal={showChatModal} - ></RunDrawer> - )} - {fileUploadVisible && ( - <JsonUploadModal - onOk={onFileUploadOk} - visible={fileUploadVisible} - hideModal={hideFileUploadModal} - ></JsonUploadModal> - )} - </div> - ); -} - -export default FlowCanvas; diff --git a/web/src/pages/flow/canvas/node/begin-node.tsx b/web/src/pages/flow/canvas/node/begin-node.tsx deleted file mode 100644 index 83e36652b..000000000 --- a/web/src/pages/flow/canvas/node/begin-node.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { IBeginNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import get from 'lodash/get'; -import { useTranslation } from 'react-i18next'; -import { - BeginQueryType, - BeginQueryTypeIconMap, - Operator, - operatorMap, -} from '../../constant'; -import { BeginQuery } from '../../interface'; -import OperatorIcon from '../../operator-icon'; -import { RightHandleStyle } from './handle-icon'; -import styles from './index.less'; - -// TODO: do not allow other nodes to connect to this node -export function BeginNode({ selected, data }: NodeProps<IBeginNode>) { - const { t } = useTranslation(); - const query: BeginQuery[] = get(data, 'form.query', []); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.ragNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - style={RightHandleStyle} - ></Handle> - - <Flex align="center" justify={'center'} gap={10}> - <OperatorIcon - name={data.label as Operator} - fontSize={24} - color={operatorMap[data.label as Operator].color} - ></OperatorIcon> - <div className="truncate text-center font-semibold text-sm"> - {t(`flow.begin`)} - </div> - </Flex> - <Flex gap={8} vertical className={styles.generateParameters}> - {query.map((x, idx) => { - const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType]; - return ( - <Flex - key={idx} - align="center" - gap={6} - className={styles.conditionBlock} - > - <Icon className="size-4" /> - <label htmlFor="">{x.key}</label> - <span className={styles.parameterValue}>{x.name}</span> - <span className="flex-1">{x.optional ? 'Yes' : 'No'}</span> - </Flex> - ); - })} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/card.tsx b/web/src/pages/flow/canvas/node/card.tsx deleted file mode 100644 index 042ca45e0..000000000 --- a/web/src/pages/flow/canvas/node/card.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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 ( - <Card className="w-[350px]"> - <CardHeader> - <CardTitle>Create project</CardTitle> - <CardDescription>Deploy your new project in one-click.</CardDescription> - </CardHeader> - <CardContent> - <form> - <div className="grid w-full items-center gap-4"> - <div className="flex flex-col space-y-1.5"> - <Label htmlFor="name">Name</Label> - <Input id="name" placeholder="Name of your project" /> - </div> - <div className="flex flex-col space-y-1.5"> - <Label htmlFor="framework">Framework</Label> - <Select> - <SelectTrigger id="framework"> - <SelectValue placeholder="Select" /> - </SelectTrigger> - <SelectContent position="popper"> - <SelectItem value="next">Next.js</SelectItem> - <SelectItem value="sveltekit">SvelteKit</SelectItem> - <SelectItem value="astro">Astro</SelectItem> - <SelectItem value="nuxt">Nuxt.js</SelectItem> - </SelectContent> - </Select> - </div> - </div> - </form> - </CardContent> - <CardFooter className="flex justify-between"> - <Button variant="outline">Cancel</Button> - <Button>Deploy</Button> - </CardFooter> - </Card> - ); -} diff --git a/web/src/pages/flow/canvas/node/categorize-handle.tsx b/web/src/pages/flow/canvas/node/categorize-handle.tsx deleted file mode 100644 index ce1fc3624..000000000 --- a/web/src/pages/flow/canvas/node/categorize-handle.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Handle, Position } from '@xyflow/react'; - -import React from 'react'; -import styles from './index.less'; - -const DEFAULT_HANDLE_STYLE = { - width: 6, - height: 6, - bottom: -5, - fontSize: 8, -}; - -interface IProps extends React.PropsWithChildren { - top: number; - right: number; - id: string; - idx?: number; -} - -const CategorizeHandle = ({ top, right, id, children }: IProps) => { - return ( - <Handle - type="source" - position={Position.Right} - id={id} - isConnectable - style={{ - ...DEFAULT_HANDLE_STYLE, - top: `${top}%`, - right: `${right}%`, - background: 'red', - color: 'black', - }} - > - <span className={styles.categorizeAnchorPointText}>{children || id}</span> - </Handle> - ); -}; - -export default CategorizeHandle; diff --git a/web/src/pages/flow/canvas/node/categorize-node.tsx b/web/src/pages/flow/canvas/node/categorize-node.tsx deleted file mode 100644 index 18c3cdff0..000000000 --- a/web/src/pages/flow/canvas/node/categorize-node.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import LLMLabel from '@/components/llm-select/llm-label'; -import { useTheme } from '@/components/theme-provider'; -import { ICategorizeNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { RightHandleStyle } from './handle-icon'; -import { useBuildCategorizeHandlePositions } from './hooks'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function CategorizeNode({ - id, - data, - selected, -}: NodeProps<ICategorizeNode>) { - const { positions } = useBuildCategorizeHandlePositions({ data, id }); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - type="target" - position={Position.Left} - isConnectable - className={styles.handle} - id={'a'} - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <Flex vertical gap={8}> - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - {positions.map((position, idx) => { - return ( - <div key={idx}> - <div className={styles.nodeText}>{position.text}</div> - <Handle - key={position.text} - id={position.text} - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - style={{ ...RightHandleStyle, top: position.top }} - ></Handle> - </div> - ); - })} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/dropdown.tsx b/web/src/pages/flow/canvas/node/dropdown.tsx deleted file mode 100644 index dd5263abc..000000000 --- a/web/src/pages/flow/canvas/node/dropdown.tsx +++ /dev/null @@ -1,58 +0,0 @@ -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: ( - <Flex justify={'space-between'}> - {t('common.copy')} - <CopyOutlined /> - </Flex> - ), - }, - ]; - - return ( - <OperateDropdown - iconFontSize={22} - height={14} - deleteItem={deleteNode} - items={items} - needsDeletionValidation={false} - iconFontColor={iconFontColor} - ></OperateDropdown> - ); -}; - -export default NodeDropdown; diff --git a/web/src/pages/flow/canvas/node/email-node.tsx b/web/src/pages/flow/canvas/node/email-node.tsx deleted file mode 100644 index ae4af848c..000000000 --- a/web/src/pages/flow/canvas/node/email-node.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { IEmailNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { useState } from 'react'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function EmailNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IEmailNode>) { - const [showDetails, setShowDetails] = useState(false); - - return ( - <section - className={classNames(styles.ragNode, { - [styles.selectedNode]: selected, - })} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - - <Flex vertical gap={8} className={styles.emailNodeContainer}> - <div - className={styles.emailConfig} - onClick={() => setShowDetails(!showDetails)} - > - <div className={styles.configItem}> - <span className={styles.configLabel}>SMTP:</span> - <span className={styles.configValue}>{data.form?.smtp_server}</span> - </div> - <div className={styles.configItem}> - <span className={styles.configLabel}>Port:</span> - <span className={styles.configValue}>{data.form?.smtp_port}</span> - </div> - <div className={styles.configItem}> - <span className={styles.configLabel}>From:</span> - <span className={styles.configValue}>{data.form?.email}</span> - </div> - <div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div> - </div> - - {showDetails && ( - <div className={styles.jsonExample}> - <div className={styles.jsonTitle}>Expected Input JSON:</div> - <pre className={styles.jsonContent}> - {`{ - "to_email": "...", - "cc_email": "...", - "subject": "...", - "content": "..." -}`} - </pre> - </div> - )} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/generate-node.tsx b/web/src/pages/flow/canvas/node/generate-node.tsx deleted file mode 100644 index 255eccd99..000000000 --- a/web/src/pages/flow/canvas/node/generate-node.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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 { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function GenerateNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IGenerateNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/handle-icon.tsx b/web/src/pages/flow/canvas/node/handle-icon.tsx deleted file mode 100644 index 36c7f3634..000000000 --- a/web/src/pages/flow/canvas/node/handle-icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { CSSProperties } from 'react'; - -export const HandleIcon = () => { - return ( - <PlusOutlined - style={{ fontSize: 6, color: 'white', position: 'absolute', zIndex: 10 }} - /> - ); -}; - -export const RightHandleStyle: CSSProperties = { - right: 0, -}; - -export const LeftHandleStyle: CSSProperties = { - left: 0, -}; - -export default HandleIcon; diff --git a/web/src/pages/flow/canvas/node/hooks.ts b/web/src/pages/flow/canvas/node/hooks.ts deleted file mode 100644 index fbea8f166..000000000 --- a/web/src/pages/flow/canvas/node/hooks.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { useUpdateNodeInternals } from '@xyflow/react'; -import get from 'lodash/get'; -import { useEffect, useMemo } from 'react'; -import { SwitchElseTo } from '../../constant'; - -import { - ICategorizeItemResult, - ISwitchCondition, - RAGFlowNodeType, -} from '@/interfaces/database/flow'; -import { generateSwitchHandleText } from '../../utils'; - -export const useBuildCategorizeHandlePositions = ({ - data, - id, -}: { - id: string; - data: RAGFlowNodeType['data']; -}) => { - const updateNodeInternals = useUpdateNodeInternals(); - - const categoryData: ICategorizeItemResult = useMemo(() => { - return get(data, `form.category_description`, {}); - }, [data]); - - const positions = useMemo(() => { - const list: Array<{ - text: string; - top: number; - idx: number; - }> = []; - - Object.keys(categoryData) - .sort((a, b) => categoryData[a].index - categoryData[b].index) - .forEach((x, idx) => { - list.push({ - text: x, - idx, - top: idx === 0 ? 98 + 20 : list[idx - 1].top + 8 + 26, - }); - }); - - return list; - }, [categoryData]); - - useEffect(() => { - updateNodeInternals(id); - }, [id, updateNodeInternals, categoryData]); - - return { positions }; -}; - -export const useBuildSwitchHandlePositions = ({ - data, - id, -}: { - id: string; - data: RAGFlowNodeType['data']; -}) => { - const updateNodeInternals = useUpdateNodeInternals(); - - const conditions: ISwitchCondition[] = useMemo(() => { - return get(data, 'form.conditions', []); - }, [data]); - - const positions = useMemo(() => { - const list: Array<{ - text: string; - top: number; - idx: number; - condition?: ISwitchCondition; - }> = []; - - [...conditions, ''].forEach((x, idx) => { - let top = idx === 0 ? 58 + 20 : list[idx - 1].top + 32; // case number (Case 1) height + flex gap - if (idx - 1 >= 0) { - const previousItems = conditions[idx - 1]?.items ?? []; - if (previousItems.length > 0) { - top += 12; // ConditionBlock padding - top += previousItems.length * 22; // condition variable height - top += (previousItems.length - 1) * 25; // operator height - } - } - - list.push({ - text: - idx < conditions.length - ? generateSwitchHandleText(idx) - : SwitchElseTo, - idx, - top, - condition: typeof x === 'string' ? undefined : x, - }); - }); - - return list; - }, [conditions]); - - useEffect(() => { - updateNodeInternals(id); - }, [id, updateNodeInternals, conditions]); - - return { positions }; -}; diff --git a/web/src/pages/flow/canvas/node/index.less b/web/src/pages/flow/canvas/node/index.less deleted file mode 100644 index 14d7e6077..000000000 --- a/web/src/pages/flow/canvas/node/index.less +++ /dev/null @@ -1,285 +0,0 @@ -.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/flow/canvas/node/index.tsx b/web/src/pages/flow/canvas/node/index.tsx deleted file mode 100644 index 32191f5cc..000000000 --- a/web/src/pages/flow/canvas/node/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { IRagNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import classNames from 'classnames'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function RagNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IRagNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.ragNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - id="b" - style={RightHandleStyle} - ></Handle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/invoke-node.tsx b/web/src/pages/flow/canvas/node/invoke-node.tsx deleted file mode 100644 index 42d109f3d..000000000 --- a/web/src/pages/flow/canvas/node/invoke-node.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { IInvokeNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { useTranslation } from 'react-i18next'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function InvokeNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IInvokeNode>) { - const { t } = useTranslation(); - const { theme } = useTheme(); - const url = get(data, 'form.url'); - return ( - <section - className={classNames( - styles.ragNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - id="b" - style={RightHandleStyle} - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - <Flex vertical> - <div>{t('flow.url')}</div> - <div className={styles.nodeText}>{url}</div> - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/iteration-node.tsx b/web/src/pages/flow/canvas/node/iteration-node.tsx deleted file mode 100644 index c15b4fc6c..000000000 --- a/web/src/pages/flow/canvas/node/iteration-node.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { - IIterationNode, - IIterationStartNode, -} from '@/interfaces/database/flow'; -import { cn } from '@/lib/utils'; -import { Handle, NodeProps, NodeResizeControl, Position } from '@xyflow/react'; -import { ListRestart } from 'lucide-react'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -function ResizeIcon() { - return ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="20" - height="20" - viewBox="0 0 24 24" - strokeWidth="2" - stroke="#5025f9" - fill="none" - strokeLinecap="round" - strokeLinejoin="round" - style={{ - position: 'absolute', - right: 5, - bottom: 5, - }} - > - <path stroke="none" d="M0 0h24v24H0z" fill="none" /> - <polyline points="16 20 20 20 20 16" /> - <line x1="14" y1="14" x2="20" y2="20" /> - <polyline points="8 4 4 4 4 8" /> - <line x1="4" y1="4" x2="10" y2="10" /> - </svg> - ); -} - -const controlStyle = { - background: 'transparent', - border: 'none', - cursor: 'nwse-resize', -}; - -export function IterationNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IIterationNode>) { - const { theme } = useTheme(); - - return ( - <section - className={cn( - 'w-full h-full bg-zinc-200 opacity-70', - styles.iterationNode, - { - ['bg-gray-800']: theme === 'dark', - [styles.selectedIterationNode]: selected, - }, - )} - > - <NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}> - <ResizeIcon /> - </NodeResizeControl> - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - id="b" - style={RightHandleStyle} - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - wrapperClassName={cn( - 'p-2 bg-white rounded-t-[10px] absolute w-full top-[-60px] left-[-0.3px]', - styles.iterationHeader, - { - [`${styles.dark} text-white`]: theme === 'dark', - [styles.selectedHeader]: selected, - }, - )} - ></NodeHeader> - </section> - ); -} - -export function IterationStartNode({ - isConnectable = true, - selected, -}: NodeProps<IIterationStartNode>) { - const { theme } = useTheme(); - - return ( - <section - className={cn('bg-white p-2 rounded-xl', { - [styles.dark]: theme === 'dark', - [styles.selectedNode]: selected, - })} - > - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - isConnectableEnd={false} - ></Handle> - <div> - <ListRestart className="size-7" /> - </div> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/keyword-node.tsx b/web/src/pages/flow/canvas/node/keyword-node.tsx deleted file mode 100644 index f607d4317..000000000 --- a/web/src/pages/flow/canvas/node/keyword-node.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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 { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function KeywordNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IKeywordNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/logic-node.tsx b/web/src/pages/flow/canvas/node/logic-node.tsx deleted file mode 100644 index 28215617b..000000000 --- a/web/src/pages/flow/canvas/node/logic-node.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { ILogicNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import classNames from 'classnames'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function LogicNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<ILogicNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/message-node.tsx b/web/src/pages/flow/canvas/node/message-node.tsx deleted file mode 100644 index 5b3a1736e..000000000 --- a/web/src/pages/flow/canvas/node/message-node.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { IMessageNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function MessageNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IMessageNode>) { - const messages: string[] = get(data, 'form.messages', []); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={classNames({ - [styles.nodeHeader]: messages.length > 0, - })} - ></NodeHeader> - - <Flex vertical gap={8} className={styles.messageNodeContainer}> - {messages.map((message, idx) => { - return ( - <div className={styles.nodeText} key={idx}> - {message} - </div> - ); - })} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/node-header.tsx b/web/src/pages/flow/canvas/node/node-header.tsx deleted file mode 100644 index 99a37dc1e..000000000 --- a/web/src/pages/flow/canvas/node/node-header.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Flex } from 'antd'; -import { Play } from 'lucide-react'; -import { Operator, operatorMap } from '../../constant'; -import OperatorIcon from '../../operator-icon'; -import { needsSingleStepDebugging } from '../../utils'; -import NodeDropdown from './dropdown'; -import { NextNodePopover } from './popover'; - -import { RunTooltip } from '../../flow-tooltip'; -interface IProps { - id: string; - label: string; - name: string; - gap?: number; - className?: string; - wrapperClassName?: string; -} - -const ExcludedRunStateOperators = [Operator.Answer]; - -export function RunStatus({ id, name, label }: IProps) { - const { t } = useTranslate('flow'); - return ( - <section className="flex justify-end items-center pb-1 gap-2 text-blue-600"> - {needsSingleStepDebugging(label) && ( - <RunTooltip> - <Play className="size-3 cursor-pointer" data-play /> - </RunTooltip> // data-play is used to trigger single step debugging - )} - <NextNodePopover nodeId={id} name={name}> - <span className="cursor-pointer text-[10px]"> - {t('operationResults')} - </span> - </NextNodePopover> - </section> - ); -} - -const NodeHeader = ({ - label, - id, - name, - gap = 4, - className, - wrapperClassName, -}: IProps) => { - return ( - <section className={wrapperClassName}> - {!ExcludedRunStateOperators.includes(label as Operator) && ( - <RunStatus id={id} name={name} label={label}></RunStatus> - )} - <Flex - flex={1} - align="center" - justify={'space-between'} - gap={gap} - className={className} - > - <OperatorIcon - name={label as Operator} - color={operatorMap[label as Operator]?.color} - ></OperatorIcon> - <span className="truncate text-center font-semibold text-sm"> - {name} - </span> - <NodeDropdown id={id} label={label}></NodeDropdown> - </Flex> - </section> - ); -}; - -export default NodeHeader; diff --git a/web/src/pages/flow/canvas/node/note-node.tsx b/web/src/pages/flow/canvas/node/note-node.tsx deleted file mode 100644 index 1917a8150..000000000 --- a/web/src/pages/flow/canvas/node/note-node.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { NodeProps, NodeResizeControl } from '@xyflow/react'; -import { Flex, Form, Input } from 'antd'; -import classNames from 'classnames'; -import NodeDropdown from './dropdown'; - -import SvgIcon from '@/components/svg-icon'; -import { useTheme } from '@/components/theme-provider'; -import { INoteNode } from '@/interfaces/database/flow'; -import { memo, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - useHandleFormValuesChange, - useHandleNodeNameChange, -} from '../../hooks'; -import styles from './index.less'; - -const { TextArea } = Input; - -const controlStyle = { - background: 'transparent', - border: 'none', -}; - -function NoteNode({ data, id }: NodeProps<INoteNode>) { - const { t } = useTranslation(); - const [form] = Form.useForm(); - const { theme } = useTheme(); - - const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({ - id, - data, - }); - const { handleValuesChange } = useHandleFormValuesChange(id); - - useEffect(() => { - form.setFieldsValue(data?.form); - }, [form, data?.form]); - - return ( - <> - <NodeResizeControl style={controlStyle} minWidth={190} minHeight={128}> - <SvgIcon - name="resize" - width={12} - style={{ - position: 'absolute', - right: 5, - bottom: 5, - cursor: 'nwse-resize', - }} - ></SvgIcon> - </NodeResizeControl> - <section - className={classNames( - styles.noteNode, - theme === 'dark' ? styles.dark : '', - )} - > - <Flex - justify={'space-between'} - className={classNames('note-drag-handle')} - align="center" - gap={6} - > - <SvgIcon name="note" width={14}></SvgIcon> - <Input - value={name ?? t('flow.note')} - onBlur={handleNameBlur} - onChange={handleNameChange} - className={styles.noteName} - ></Input> - <NodeDropdown id={id} label={data.label}></NodeDropdown> - </Flex> - <Form - onValuesChange={handleValuesChange} - form={form} - className={styles.noteForm} - > - <Form.Item name="text" noStyle> - <TextArea - rows={3} - placeholder={t('flow.notePlaceholder')} - className={styles.noteTextarea} - /> - </Form.Item> - </Form> - </section> - </> - ); -} - -export default memo(NoteNode); diff --git a/web/src/pages/flow/canvas/node/popover.tsx b/web/src/pages/flow/canvas/node/popover.tsx deleted file mode 100644 index 871aa8064..000000000 --- a/web/src/pages/flow/canvas/node/popover.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; -import get from 'lodash/get'; -import React, { - MouseEventHandler, - useCallback, - useMemo, - useState, -} from 'react'; -import JsonView from 'react18-json-view'; -import 'react18-json-view/src/style.css'; -import { useReplaceIdWithText } from '../../hooks'; - -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { useTranslate } from '@/hooks/common-hooks'; -import { - Button, - Card, - Col, - Input, - Row, - Space, - Tabs, - Typography, - message, -} from 'antd'; -import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; - -interface IProps extends React.PropsWithChildren { - nodeId: string; - name?: string; -} - -export function NextNodePopover({ children, nodeId, name }: IProps) { - const { t } = useTranslate('flow'); - - const { data } = useFetchFlow(); - console.log(data); - - const component = useMemo(() => { - return get(data, ['dsl', 'components', nodeId], {}); - }, [nodeId, data]); - - const inputs: Array<{ component_id: string; content: string }> = get( - component, - ['obj', 'inputs'], - [], - ); - const output = get(component, ['obj', 'output'], {}); - const { conf, messages, prompt } = get( - component, - ['obj', 'params', 'infor'], - {}, - ); - const { replacedOutput } = useReplaceIdWithText(output); - const stopPropagation: MouseEventHandler = useCallback((e) => { - e.stopPropagation(); - }, []); - - const getLabel = useGetComponentLabelByValue(nodeId); - - const [inputPage, setInputPage] = useState(1); - const pageSize = 3; - const pagedInputs = inputs.slice( - (inputPage - 1) * pageSize, - inputPage * pageSize, - ); - - return ( - <Popover> - <PopoverTrigger onClick={stopPropagation} asChild> - {children} - </PopoverTrigger> - <PopoverContent - align={'start'} - side={'right'} - sideOffset={20} - onClick={stopPropagation} - className="w-[800px] p-4" - style={{ maxHeight: 600, overflow: 'auto' }} - > - <Card - bordered={false} - style={{ marginBottom: 16, padding: 0 }} - bodyStyle={{ padding: 0 }} - > - <Typography.Title - level={5} - style={{ - marginBottom: 16, - fontWeight: 600, - fontSize: 18, - borderBottom: '1px solid #f0f0f0', - paddingBottom: 8, - }} - > - {name} {t('operationResults')} - </Typography.Title> - </Card> - <Tabs - defaultActiveKey="input" - items={[ - { - key: 'input', - label: t('input'), - children: ( - <Card - size="small" - className="bg-gray-50 dark:bg-gray-800" - style={{ borderRadius: 8, border: '1px solid #e5e7eb' }} - bodyStyle={{ padding: 16 }} - > - <Table> - <TableHeader> - <TableRow> - <TableHead>{t('componentId')}</TableHead> - <TableHead className="w-[60px]"> - {t('content')} - </TableHead> - </TableRow> - </TableHeader> - <TableBody> - {pagedInputs.map((x, idx) => ( - <TableRow key={idx + (inputPage - 1) * pageSize}> - <TableCell>{getLabel(x.component_id)}</TableCell> - <TableCell className="truncate"> - {x.content} - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - {/* Pagination */} - {inputs.length > pageSize && ( - <Row justify="end" style={{ marginTop: 8 }}> - <Space> - <Button - size="small" - disabled={inputPage === 1} - onClick={() => setInputPage(inputPage - 1)} - > - Prev - </Button> - <span className="mx-2 text-sm"> - {inputPage} / {Math.ceil(inputs.length / pageSize)} - </span> - <Button - size="small" - disabled={ - inputPage === Math.ceil(inputs.length / pageSize) - } - onClick={() => setInputPage(inputPage + 1)} - > - Next - </Button> - </Space> - </Row> - )} - </Card> - ), - }, - { - key: 'output', - label: t('output'), - children: ( - <Card - size="small" - className="bg-gray-50 dark:bg-gray-800" - style={{ borderRadius: 8, border: '1px solid #e5e7eb' }} - bodyStyle={{ padding: 16 }} - > - <JsonView - src={replacedOutput} - displaySize={30} - className="w-full max-h-[300px] break-words overflow-auto" - /> - </Card> - ), - }, - { - key: 'infor', - label: t('infor'), - children: ( - <Card - size="small" - className="bg-gray-50 dark:bg-gray-800" - style={{ borderRadius: 8, border: '1px solid #e5e7eb' }} - bodyStyle={{ padding: 16 }} - > - <Row gutter={16}> - <Col span={12}> - {conf && ( - <Card - size="small" - bordered={false} - style={{ - marginBottom: 16, - background: 'transparent', - }} - bodyStyle={{ padding: 0 }} - > - <Typography.Text - strong - style={{ - color: '#888', - marginBottom: 8, - display: 'block', - }} - > - Configuration: - </Typography.Text> - <JsonView - src={conf} - displaySize={30} - className="w-full max-h-[120px] break-words overflow-auto" - /> - </Card> - )} - {prompt && ( - <Card - size="small" - bordered={false} - style={{ background: 'transparent' }} - bodyStyle={{ padding: 0 }} - > - <Row - align="middle" - justify="space-between" - style={{ marginBottom: 8 }} - > - <Col> - <Typography.Text strong style={{ color: '#888' }}> - Prompt: - </Typography.Text> - </Col> - <Col> - <Button - size="small" - onClick={() => { - const inlineString = prompt - .replace(/\s+/g, ' ') - .trim(); - navigator.clipboard.writeText(inlineString); - message.success( - 'Prompt copied as single line!', - ); - }} - > - Copy as single line - </Button> - </Col> - </Row> - <Input.TextArea - value={prompt} - readOnly - autoSize={{ minRows: 2, maxRows: 6 }} - className="bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700" - /> - </Card> - )} - </Col> - <Col span={12}> - {messages && ( - <Card - size="small" - bordered={false} - style={{ - marginBottom: 16, - background: 'transparent', - }} - bodyStyle={{ padding: 0 }} - > - <Typography.Text - strong - style={{ - color: '#888', - marginBottom: 8, - display: 'block', - }} - > - Messages: - </Typography.Text> - <div className="max-h-[300px] overflow-auto"> - <JsonView - src={messages} - displaySize={30} - className="w-full break-words" - /> - </div> - </Card> - )} - </Col> - </Row> - </Card> - ), - }, - ]} - /> - </PopoverContent> - </Popover> - ); -} diff --git a/web/src/pages/flow/canvas/node/relevant-node.tsx b/web/src/pages/flow/canvas/node/relevant-node.tsx deleted file mode 100644 index acc098d69..000000000 --- a/web/src/pages/flow/canvas/node/relevant-node.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { RightHandleStyle } from './handle-icon'; - -import { useTheme } from '@/components/theme-provider'; -import { IRelevantNode } from '@/interfaces/database/flow'; -import { get } from 'lodash'; -import { useReplaceIdWithName } from '../../hooks'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function RelevantNode({ id, data, selected }: NodeProps<IRelevantNode>) { - const yes = get(data, 'form.yes'); - const no = get(data, 'form.no'); - const replaceIdWithName = useReplaceIdWithName(); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - type="target" - position={Position.Left} - isConnectable - className={styles.handle} - id={'a'} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - id={'yes'} - style={{ ...RightHandleStyle, top: 57 + 20 }} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - id={'no'} - style={{ ...RightHandleStyle, top: 115 + 20 }} - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <Flex vertical gap={10}> - <Flex vertical> - <div className={styles.relevantLabel}>Yes</div> - <div className={styles.nodeText}>{replaceIdWithName(yes)}</div> - </Flex> - <Flex vertical> - <div className={styles.relevantLabel}>No</div> - <div className={styles.nodeText}>{replaceIdWithName(no)}</div> - </Flex> - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/retrieval-node.tsx b/web/src/pages/flow/canvas/node/retrieval-node.tsx deleted file mode 100644 index e9ffd669a..000000000 --- a/web/src/pages/flow/canvas/node/retrieval-node.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; -import { IRetrievalNode } from '@/interfaces/database/flow'; -import { UserOutlined } from '@ant-design/icons'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Avatar, Button, Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { useMemo, useState } from 'react'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function RetrievalNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IRetrievalNode>) { - const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); - const { theme } = useTheme(); - const { list: knowledgeList } = useFetchKnowledgeList(true); - const [showAllKnowledge, setShowAllKnowledge] = useState(false); - const knowledgeBases = useMemo(() => { - return knowledgeBaseIds.map((x) => { - const item = knowledgeList.find((y) => x === y.id); - return { - name: item?.name, - avatar: item?.avatar, - id: x, - }; - }); - }, [knowledgeList, knowledgeBaseIds]); - - const displayedKnowledgeBases = showAllKnowledge - ? knowledgeBases - : knowledgeBases.slice(0, 3); - function showAllItem(e: any) { - e.stopPropagation(); // Prevent event from bubbling to parent - setShowAllKnowledge(!showAllKnowledge); - } - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={classNames({ - [styles.nodeHeader]: knowledgeBaseIds.length > 0, - })} - ></NodeHeader> - <Flex vertical gap={8}> - {displayedKnowledgeBases.map((knowledge) => { - return ( - <div className={styles.nodeText} key={knowledge.id}> - <Flex align={'center'} gap={6}> - <Avatar - size={26} - icon={<UserOutlined />} - src={knowledge.avatar} - /> - <Flex className={styles.knowledgeNodeName} flex={1}> - {knowledge.name} - </Flex> - </Flex> - </div> - ); - })} - {knowledgeBases.length > 3 && ( - <div className={styles.nodeText}> - <Button - type="link" - size="small" - onClick={showAllItem} - style={{ - padding: 0, - height: 'auto', - lineHeight: '1.5', - textAlign: 'left', - }} - > - {showAllKnowledge - ? 'Hide' - : `Show more ${knowledgeBases.length - 3} knowledge`} - </Button> - </div> - )} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/rewrite-node.tsx b/web/src/pages/flow/canvas/node/rewrite-node.tsx deleted file mode 100644 index 093b2c80e..000000000 --- a/web/src/pages/flow/canvas/node/rewrite-node.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import LLMLabel from '@/components/llm-select/llm-label'; -import { useTheme } from '@/components/theme-provider'; -import { IRewriteNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -export function RewriteNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<IRewriteNode>) { - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <div className={styles.nodeText}> - <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> - </div> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/switch-node.tsx b/web/src/pages/flow/canvas/node/switch-node.tsx deleted file mode 100644 index 860a0ba96..000000000 --- a/web/src/pages/flow/canvas/node/switch-node.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Divider, Flex } from 'antd'; -import classNames from 'classnames'; -import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; -import { RightHandleStyle } from './handle-icon'; -import { useBuildSwitchHandlePositions } from './hooks'; -import styles from './index.less'; -import NodeHeader from './node-header'; - -const getConditionKey = (idx: number, length: number) => { - if (idx === 0 && length !== 1) { - return 'If'; - } else if (idx === length - 1) { - return 'Else'; - } - - return 'ElseIf'; -}; - -const ConditionBlock = ({ - condition, - nodeId, -}: { - condition: ISwitchCondition; - nodeId: string; -}) => { - const items = condition?.items ?? []; - const getLabel = useGetComponentLabelByValue(nodeId); - return ( - <Flex vertical className={styles.conditionBlock}> - {items.map((x, idx) => ( - <div key={idx}> - <Flex> - <div - className={classNames(styles.conditionLine, styles.conditionKey)} - > - {getLabel(x?.cpn_id)} - </div> - <span className={styles.conditionOperator}>{x?.operator}</span> - <Flex flex={1} className={styles.conditionLine}> - {x?.value} - </Flex> - </Flex> - {idx + 1 < items.length && ( - <Divider orientationMargin="0" className={styles.zeroDivider}> - {condition?.logical_operator} - </Divider> - )} - </div> - ))} - </Flex> - ); -}; - -export function SwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { - const { positions } = useBuildSwitchHandlePositions({ data, id }); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - type="target" - position={Position.Left} - isConnectable - className={styles.handle} - id={'a'} - ></Handle> - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - <Flex vertical gap={10}> - {positions.map((position, idx) => { - return ( - <div key={idx}> - <Flex vertical> - <Flex justify={'space-between'}> - <span>{idx < positions.length - 1 && position.text}</span> - <span>{getConditionKey(idx, positions.length)}</span> - </Flex> - {position.condition && ( - <ConditionBlock - nodeId={id} - condition={position.condition} - ></ConditionBlock> - )} - </Flex> - <Handle - key={position.text} - id={position.text} - type="source" - position={Position.Right} - isConnectable - className={styles.handle} - style={{ ...RightHandleStyle, top: position.top }} - ></Handle> - </div> - ); - })} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/canvas/node/template-node.tsx b/web/src/pages/flow/canvas/node/template-node.tsx deleted file mode 100644 index 971fbab38..000000000 --- a/web/src/pages/flow/canvas/node/template-node.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useTheme } from '@/components/theme-provider'; -import { Handle, NodeProps, Position } from '@xyflow/react'; -import { Flex } from 'antd'; -import classNames from 'classnames'; -import { get } from 'lodash'; -import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; -import { IGenerateParameter } from '../../interface'; -import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; -import NodeHeader from './node-header'; - -import { ITemplateNode } from '@/interfaces/database/flow'; -import styles from './index.less'; - -export function TemplateNode({ - id, - data, - isConnectable = true, - selected, -}: NodeProps<ITemplateNode>) { - const parameters: IGenerateParameter[] = get(data, 'form.parameters', []); - const getLabel = useGetComponentLabelByValue(id); - const { theme } = useTheme(); - return ( - <section - className={classNames( - styles.logicNode, - theme === 'dark' ? styles.dark : '', - - { - [styles.selectedNode]: selected, - }, - )} - > - <Handle - id="c" - type="source" - position={Position.Left} - isConnectable={isConnectable} - className={styles.handle} - style={LeftHandleStyle} - ></Handle> - <Handle - type="source" - position={Position.Right} - isConnectable={isConnectable} - className={styles.handle} - style={RightHandleStyle} - id="b" - ></Handle> - - <NodeHeader - id={id} - name={data.name} - label={data.label} - className={styles.nodeHeader} - ></NodeHeader> - - <Flex gap={8} vertical className={styles.generateParameters}> - {parameters.map((x) => ( - <Flex - key={x.id} - align="center" - gap={6} - className={styles.conditionBlock} - > - <label htmlFor="">{x.key}</label> - <span className={styles.parameterValue}> - {getLabel(x.component_id)} - </span> - </Flex> - ))} - </Flex> - </section> - ); -} diff --git a/web/src/pages/flow/chat/box.tsx b/web/src/pages/flow/chat/box.tsx deleted file mode 100644 index a7196130c..000000000 --- a/web/src/pages/flow/chat/box.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import MessageItem from '@/components/message-item'; -import { MessageType } from '@/constants/chat'; -import { useGetFileIcon } from '@/pages/chat/hooks'; -import { buildMessageItemReference } from '@/pages/chat/utils'; -import { Flex, Spin } from 'antd'; - -import { useSendNextMessage } from './hooks'; - -import MessageInput from '@/components/message-input'; -import PdfDrawer from '@/components/pdf-drawer'; -import { useClickDrawer } from '@/components/pdf-drawer/hooks'; -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; -import { buildMessageUuidWithRole } from '@/utils/chat'; -import styles from './index.less'; - -const FlowChatBox = () => { - const { - sendLoading, - handleInputChange, - handlePressEnter, - value, - loading, - ref, - derivedMessages, - reference, - stopOutputMessage, - } = useSendNextMessage(); - - const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = - useClickDrawer(); - useGetFileIcon(); - const { data: userInfo } = useFetchUserInfo(); - const { data: canvasInfo } = useFetchFlow(); - - return ( - <> - <Flex flex={1} className={styles.chatContainer} vertical> - <Flex flex={1} vertical className={styles.messageContainer}> - <div> - <Spin spinning={loading}> - {derivedMessages?.map((message, i) => { - return ( - <MessageItem - loading={ - message.role === MessageType.Assistant && - sendLoading && - derivedMessages.length - 1 === i - } - key={buildMessageUuidWithRole(message)} - nickname={userInfo.nickname} - avatar={userInfo.avatar} - avatarDialog={canvasInfo.avatar} - item={message} - reference={buildMessageItemReference( - { message: derivedMessages, reference }, - message, - )} - clickDocumentButton={clickDocumentButton} - index={i} - showLikeButton={false} - sendLoading={sendLoading} - ></MessageItem> - ); - })} - </Spin> - </div> - <div ref={ref} /> - </Flex> - <MessageInput - showUploadIcon={false} - value={value} - sendLoading={sendLoading} - disabled={false} - sendDisabled={sendLoading} - conversationId="" - onPressEnter={handlePressEnter} - onInputChange={handleInputChange} - stopOutputMessage={stopOutputMessage} - /> - </Flex> - <PdfDrawer - visible={visible} - hideModal={hideModal} - documentId={documentId} - chunk={selectedChunk} - ></PdfDrawer> - </> - ); -}; - -export default FlowChatBox; diff --git a/web/src/pages/flow/chat/drawer.tsx b/web/src/pages/flow/chat/drawer.tsx deleted file mode 100644 index e86422957..000000000 --- a/web/src/pages/flow/chat/drawer.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { Drawer } from 'antd'; -import { getDrawerWidth } from '../utils'; -import FlowChatBox from './box'; - -const ChatDrawer = ({ visible, hideModal }: IModalProps<any>) => { - const { data } = useFetchFlow(); - - return ( - <Drawer - title={data.title} - placement="right" - onClose={hideModal} - open={visible} - getContainer={false} - width={getDrawerWidth()} - mask={false} - > - <FlowChatBox></FlowChatBox> - </Drawer> - ); -}; - -export default ChatDrawer; diff --git a/web/src/pages/flow/chat/hooks.ts b/web/src/pages/flow/chat/hooks.ts deleted file mode 100644 index dc4fda33d..000000000 --- a/web/src/pages/flow/chat/hooks.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { MessageType } from '@/constants/chat'; -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { - useHandleMessageInputChange, - useSelectDerivedMessages, - useSendMessageWithSse, -} from '@/hooks/logic-hooks'; -import { Message } from '@/interfaces/database/chat'; -import i18n from '@/locales/config'; -import api from '@/utils/api'; -import { message } from 'antd'; -import trim from 'lodash/trim'; -import { useCallback, useEffect } from 'react'; -import { useParams } from 'umi'; -import { v4 as uuid } from 'uuid'; -import { receiveMessageError } from '../utils'; - -const antMessage = message; - -export const useSelectNextMessages = () => { - const { data: flowDetail, loading } = useFetchFlow(); - const reference = flowDetail.dsl.reference; - const { - derivedMessages, - ref, - addNewestQuestion, - addNewestAnswer, - removeLatestMessage, - removeMessageById, - removeMessagesAfterCurrentMessage, - } = useSelectDerivedMessages(); - - return { - reference, - loading, - derivedMessages, - ref, - addNewestQuestion, - addNewestAnswer, - removeLatestMessage, - removeMessageById, - removeMessagesAfterCurrentMessage, - }; -}; - -export const useSendNextMessage = () => { - const { - reference, - loading, - derivedMessages, - ref, - addNewestQuestion, - addNewestAnswer, - removeLatestMessage, - removeMessageById, - } = useSelectNextMessages(); - const { id: flowId } = useParams(); - const { handleInputChange, value, setValue } = useHandleMessageInputChange(); - const { refetch } = useFetchFlow(); - - const { send, answer, done, stopOutputMessage } = useSendMessageWithSse( - api.runCanvas, - ); - - const sendMessage = useCallback( - async ({ message }: { message: Message; messages?: Message[] }) => { - const params: Record<string, unknown> = { - id: flowId, - }; - params.running_hint_text = i18n.t('flow.runningHintText', { - defaultValue: 'is running...🕞', - }); - if (message.content) { - params.message = message.content; - params.message_id = message.id; - } - const res = await send(params); - - if (receiveMessageError(res)) { - antMessage.error(res?.data?.message); - - // cancel loading - setValue(message.content); - removeLatestMessage(); - } else { - refetch(); // pull the message list after sending the message successfully - } - }, - [flowId, send, setValue, removeLatestMessage, refetch], - ); - - const handleSendMessage = useCallback( - async (message: Message) => { - sendMessage({ message }); - }, - [sendMessage], - ); - - useEffect(() => { - if (answer.answer) { - addNewestAnswer(answer); - } - }, [answer, addNewestAnswer]); - - const handlePressEnter = useCallback(() => { - if (trim(value) === '') return; - const id = uuid(); - if (done) { - setValue(''); - handleSendMessage({ id, content: value.trim(), role: MessageType.User }); - } - addNewestQuestion({ - content: value, - id, - role: MessageType.User, - }); - }, [addNewestQuestion, handleSendMessage, done, setValue, value]); - - const fetchPrologue = useCallback(async () => { - // fetch prologue - const sendRet = await send({ id: flowId }); - if (receiveMessageError(sendRet)) { - message.error(sendRet?.data?.message); - } else { - refetch(); - } - }, [flowId, refetch, send]); - - useEffect(() => { - fetchPrologue(); - }, [fetchPrologue]); - - return { - handlePressEnter, - handleInputChange, - value, - sendLoading: !done, - reference, - loading, - derivedMessages, - ref, - removeMessageById, - stopOutputMessage, - }; -}; diff --git a/web/src/pages/flow/chat/index.less b/web/src/pages/flow/chat/index.less deleted file mode 100644 index ee2b21bff..000000000 --- a/web/src/pages/flow/chat/index.less +++ /dev/null @@ -1,8 +0,0 @@ -.chatContainer { - padding: 0; - height: 100%; - .messageContainer { - overflow-y: auto; - padding-right: 24px; - } -} diff --git a/web/src/pages/flow/constant.tsx b/web/src/pages/flow/constant.tsx deleted file mode 100644 index fe16e4741..000000000 --- a/web/src/pages/flow/constant.tsx +++ /dev/null @@ -1,3006 +0,0 @@ -import { - GitHubIcon, - KeywordIcon, - QWeatherIcon, - WikipediaIcon, -} from '@/assets/icon/next-icon'; -import { ReactComponent as AkShareIcon } from '@/assets/svg/akshare.svg'; -import { ReactComponent as ArXivIcon } from '@/assets/svg/arxiv.svg'; -import { ReactComponent as baiduFanyiIcon } from '@/assets/svg/baidu-fanyi.svg'; -import { ReactComponent as BaiduIcon } from '@/assets/svg/baidu.svg'; -import { ReactComponent as BeginIcon } from '@/assets/svg/begin.svg'; -import { ReactComponent as BingIcon } from '@/assets/svg/bing.svg'; -import { ReactComponent as ConcentratorIcon } from '@/assets/svg/concentrator.svg'; -import { ReactComponent as CrawlerIcon } from '@/assets/svg/crawler.svg'; -import { ReactComponent as DeepLIcon } from '@/assets/svg/deepl.svg'; -import { ReactComponent as DuckIcon } from '@/assets/svg/duck.svg'; -import { ReactComponent as EmailIcon } from '@/assets/svg/email.svg'; -import { ReactComponent as ExeSqlIcon } from '@/assets/svg/exesql.svg'; -import { ReactComponent as GoogleScholarIcon } from '@/assets/svg/google-scholar.svg'; -import { ReactComponent as GoogleIcon } from '@/assets/svg/google.svg'; -import { ReactComponent as InvokeIcon } from '@/assets/svg/invoke-ai.svg'; -import { ReactComponent as Jin10Icon } from '@/assets/svg/jin10.svg'; -import { ReactComponent as NoteIcon } from '@/assets/svg/note.svg'; -import { ReactComponent as PubMedIcon } from '@/assets/svg/pubmed.svg'; -import { ReactComponent as SwitchIcon } from '@/assets/svg/switch.svg'; -import { ReactComponent as TemplateIcon } from '@/assets/svg/template.svg'; -import { ReactComponent as TuShareIcon } from '@/assets/svg/tushare.svg'; -import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; -import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; -import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; - -// 邮件功能 - -import { - ChatVariableEnabledField, - variableEnabledFieldMap, -} from '@/constants/chat'; -import i18n from '@/locales/config'; -import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat'; - -// DuckDuckGo's channel options -export enum Channel { - Text = 'text', - News = 'news', -} - -import { - BranchesOutlined, - DatabaseOutlined, - FormOutlined, - MergeCellsOutlined, - MessageOutlined, - RocketOutlined, - SendOutlined, -} from '@ant-design/icons'; -import upperFirst from 'lodash/upperFirst'; -import { - CirclePower, - CloudUpload, - CodeXml, - Database, - IterationCcw, - ListOrdered, - OptionIcon, - TextCursorInput, - ToggleLeft, - WrapText, -} from 'lucide-react'; - -export const BeginId = 'begin'; - -export enum Operator { - Begin = 'Begin', - Retrieval = 'Retrieval', - Generate = 'Generate', - Answer = 'Answer', - Categorize = 'Categorize', - Message = 'Message', - Relevant = 'Relevant', - RewriteQuestion = 'RewriteQuestion', - KeywordExtract = 'KeywordExtract', - Baidu = 'Baidu', - DuckDuckGo = 'DuckDuckGo', - Wikipedia = 'Wikipedia', - PubMed = 'PubMed', - ArXiv = 'ArXiv', - Google = 'Google', - Bing = 'Bing', - GoogleScholar = 'GoogleScholar', - DeepL = 'DeepL', - GitHub = 'GitHub', - BaiduFanyi = 'BaiduFanyi', - QWeather = 'QWeather', - ExeSQL = 'ExeSQL', - Switch = 'Switch', - WenCai = 'WenCai', - AkShare = 'AkShare', - YahooFinance = 'YahooFinance', - Jin10 = 'Jin10', - Concentrator = 'Concentrator', - TuShare = 'TuShare', - Note = 'Note', - Crawler = 'Crawler', - Invoke = 'Invoke', - Template = 'Template', - Email = 'Email', - Iteration = 'Iteration', - IterationStart = 'IterationItem', - Code = 'Code', -} - -export const CommonOperatorList = Object.values(Operator).filter( - (x) => x !== Operator.Note, -); - -export const operatorIconMap = { - [Operator.Retrieval]: RocketOutlined, - [Operator.Generate]: MergeCellsOutlined, - [Operator.Answer]: SendOutlined, - [Operator.Begin]: BeginIcon, - [Operator.Categorize]: DatabaseOutlined, - [Operator.Message]: MessageOutlined, - [Operator.Relevant]: BranchesOutlined, - [Operator.RewriteQuestion]: FormOutlined, - [Operator.KeywordExtract]: KeywordIcon, - [Operator.DuckDuckGo]: DuckIcon, - [Operator.Baidu]: BaiduIcon, - [Operator.Wikipedia]: WikipediaIcon, - [Operator.PubMed]: PubMedIcon, - [Operator.ArXiv]: ArXivIcon, - [Operator.Google]: GoogleIcon, - [Operator.Bing]: BingIcon, - [Operator.GoogleScholar]: GoogleScholarIcon, - [Operator.DeepL]: DeepLIcon, - [Operator.GitHub]: GitHubIcon, - [Operator.BaiduFanyi]: baiduFanyiIcon, - [Operator.QWeather]: QWeatherIcon, - [Operator.ExeSQL]: ExeSqlIcon, - [Operator.Switch]: SwitchIcon, - [Operator.WenCai]: WenCaiIcon, - [Operator.AkShare]: AkShareIcon, - [Operator.YahooFinance]: YahooFinanceIcon, - [Operator.Jin10]: Jin10Icon, - [Operator.Concentrator]: ConcentratorIcon, - [Operator.TuShare]: TuShareIcon, - [Operator.Note]: NoteIcon, - [Operator.Crawler]: CrawlerIcon, - [Operator.Invoke]: InvokeIcon, - [Operator.Template]: TemplateIcon, - [Operator.Email]: EmailIcon, - [Operator.Iteration]: IterationCcw, - [Operator.IterationStart]: CirclePower, - [Operator.Code]: CodeXml, -}; - -export const operatorMap: Record< - Operator, - { - backgroundColor?: string; - color?: string; - width?: number; - height?: number; - fontSize?: number; - iconFontSize?: number; - iconWidth?: number; - moreIconColor?: string; - } -> = { - [Operator.Retrieval]: { - backgroundColor: '#cad6e0', - color: '#385974', - }, - [Operator.Generate]: { - backgroundColor: '#ebd6d6', - width: 150, - height: 150, - fontSize: 20, - iconFontSize: 30, - color: '#996464', - }, - [Operator.Answer]: { - backgroundColor: '#f4816d', - color: '#f4816d', - }, - [Operator.Begin]: { - backgroundColor: '#4f51d6', - }, - [Operator.Categorize]: { - backgroundColor: '#ffebcd', - color: '#cc8a26', - }, - [Operator.Message]: { - backgroundColor: '#c5ddc7', - color: 'green', - }, - [Operator.Relevant]: { - backgroundColor: '#9fd94d', - color: '#8ef005', - width: 70, - height: 70, - fontSize: 12, - iconFontSize: 16, - }, - [Operator.RewriteQuestion]: { - backgroundColor: '#f8c7f8', - color: '#f32bf3', - width: 70, - height: 70, - fontSize: 12, - iconFontSize: 16, - }, - [Operator.KeywordExtract]: { - width: 70, - height: 70, - backgroundColor: '#6E5494', - color: '#6E5494', - fontSize: 12, - iconWidth: 16, - }, - [Operator.DuckDuckGo]: { - backgroundColor: '#e7e389', - color: '#aea00c', - }, - [Operator.Baidu]: { - backgroundColor: '#d9e0f8', - }, - [Operator.Wikipedia]: { - backgroundColor: '#dee0e2', - }, - [Operator.PubMed]: { - backgroundColor: '#a2ccf0', - }, - [Operator.ArXiv]: { - width: 70, - height: 70, - fontSize: 12, - iconWidth: 16, - iconFontSize: 16, - moreIconColor: 'white', - backgroundColor: '#b31b1b', - color: 'white', - }, - [Operator.Google]: { - backgroundColor: 'pink', - }, - [Operator.Bing]: { - backgroundColor: '#c0dcc4', - }, - [Operator.GoogleScholar]: { - backgroundColor: '#b4e4f6', - }, - [Operator.DeepL]: { - backgroundColor: '#f5e8e6', - }, - [Operator.GitHub]: { - backgroundColor: 'purple', - color: 'purple', - }, - [Operator.BaiduFanyi]: { backgroundColor: '#e5f2d3' }, - [Operator.QWeather]: { - backgroundColor: '#a4bbf3', - color: '#a4bbf3', - }, - [Operator.ExeSQL]: { backgroundColor: '#b9efe8' }, - [Operator.Switch]: { backgroundColor: '#dbaff6', color: '#dbaff6' }, - [Operator.WenCai]: { backgroundColor: '#faac5b' }, - [Operator.AkShare]: { backgroundColor: '#8085f5' }, - [Operator.YahooFinance]: { backgroundColor: '#b474ff' }, - [Operator.Jin10]: { backgroundColor: '#a0b9f8' }, - [Operator.Concentrator]: { - backgroundColor: '#32d2a3', - color: '#32d2a3', - width: 70, - height: 70, - fontSize: 10, - iconFontSize: 16, - }, - [Operator.TuShare]: { backgroundColor: '#f8cfa0' }, - [Operator.Note]: { backgroundColor: '#f8cfa0' }, - [Operator.Crawler]: { - backgroundColor: '#dee0e2', - }, - [Operator.Invoke]: { - backgroundColor: '#dee0e2', - }, - [Operator.Template]: { - backgroundColor: '#dee0e2', - }, - [Operator.Email]: { backgroundColor: '#e6f7ff' }, - [Operator.Iteration]: { backgroundColor: '#e6f7ff' }, - [Operator.IterationStart]: { backgroundColor: '#e6f7ff' }, - [Operator.Code]: { backgroundColor: '#4c5458' }, -}; - -export const componentMenuList = [ - { - name: Operator.Retrieval, - }, - { - name: Operator.Generate, - }, - { - name: Operator.Answer, - }, - { - name: Operator.Categorize, - }, - { - name: Operator.Message, - }, - - { - name: Operator.RewriteQuestion, - }, - { - name: Operator.KeywordExtract, - }, - { - name: Operator.Switch, - }, - { - name: Operator.Concentrator, - }, - { - name: Operator.Template, - }, - { - name: Operator.Iteration, - }, - { - name: Operator.Code, - }, - { - name: Operator.Note, - }, - { - name: Operator.DuckDuckGo, - }, - { - name: Operator.Baidu, - }, - { - name: Operator.Wikipedia, - }, - { - name: Operator.PubMed, - }, - { - name: Operator.ArXiv, - }, - { - name: Operator.Google, - }, - { - name: Operator.Bing, - }, - { - name: Operator.GoogleScholar, - }, - { - name: Operator.DeepL, - }, - { - name: Operator.GitHub, - }, - { - name: Operator.BaiduFanyi, - }, - { - name: Operator.QWeather, - }, - { - name: Operator.ExeSQL, - }, - { - name: Operator.WenCai, - }, - { - name: Operator.AkShare, - }, - { - name: Operator.YahooFinance, - }, - { - name: Operator.Jin10, - }, - { - name: Operator.TuShare, - }, - { - name: Operator.Crawler, - }, - { - name: Operator.Invoke, - }, - { - name: Operator.Email, - }, -]; - -const initialQueryBaseValues = { - query: [], -}; - -export const initialRetrievalValues = { - similarity_threshold: 0.2, - keywords_similarity_weight: 0.3, - top_n: 8, - use_kg: false, - ...initialQueryBaseValues, -}; - -export const initialBeginValues = { - prologue: `Hi! I'm your assistant. What can I do for you?`, -}; - -export const variableCheckBoxFieldMap = Object.keys( - variableEnabledFieldMap, -).reduce<Record<string, boolean>>((pre, cur) => { - pre[cur] = setInitialChatVariableEnabledFieldValue( - cur as ChatVariableEnabledField, - ); - return pre; -}, {}); - -const initialLlmBaseValues = { - ...variableCheckBoxFieldMap, - temperature: 0.1, - top_p: 0.3, - frequency_penalty: 0.7, - presence_penalty: 0.4, - max_tokens: 256, -}; - -export const initialGenerateValues = { - ...initialLlmBaseValues, - prompt: i18n.t('flow.promptText'), - cite: true, - message_history_window_size: 12, - parameters: [], -}; - -export const initialRewriteQuestionValues = { - ...initialLlmBaseValues, - language: '', - message_history_window_size: 6, -}; - -export const initialRelevantValues = { - ...initialLlmBaseValues, -}; - -export const initialCategorizeValues = { - ...initialLlmBaseValues, - message_history_window_size: 1, - category_description: {}, - ...initialQueryBaseValues, -}; - -export const initialMessageValues = { - messages: [], -}; - -export const initialKeywordExtractValues = { - ...initialLlmBaseValues, - top_n: 3, - ...initialQueryBaseValues, -}; -export const initialDuckValues = { - top_n: 10, - channel: Channel.Text, - ...initialQueryBaseValues, -}; - -export const initialBaiduValues = { - top_n: 10, - ...initialQueryBaseValues, -}; - -export const initialWikipediaValues = { - top_n: 10, - language: 'en', - ...initialQueryBaseValues, -}; - -export const initialPubMedValues = { - top_n: 10, - email: '', - ...initialQueryBaseValues, -}; - -export const initialArXivValues = { - top_n: 10, - sort_by: 'relevance', - ...initialQueryBaseValues, -}; - -export const initialGoogleValues = { - top_n: 10, - api_key: 'YOUR_API_KEY (obtained from https://serpapi.com/manage-api-key)', - country: 'cn', - language: 'en', - ...initialQueryBaseValues, -}; - -export const initialBingValues = { - top_n: 10, - channel: 'Webpages', - api_key: - 'YOUR_API_KEY (obtained from https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)', - country: 'CH', - language: 'en', - ...initialQueryBaseValues, -}; - -export const initialGoogleScholarValues = { - top_n: 5, - sort_by: 'relevance', - patents: true, - ...initialQueryBaseValues, -}; - -export const initialDeepLValues = { - top_n: 5, - auth_key: 'relevance', -}; - -export const initialGithubValues = { - top_n: 5, - ...initialQueryBaseValues, -}; - -export const initialBaiduFanyiValues = { - appid: 'xxx', - secret_key: 'xxx', - trans_type: 'translate', - ...initialQueryBaseValues, -}; - -export const initialQWeatherValues = { - web_apikey: 'xxx', - type: 'weather', - user_type: 'free', - time_period: 'now', - ...initialQueryBaseValues, -}; - -export const initialExeSqlValues = { - ...initialLlmBaseValues, - db_type: 'mysql', - database: '', - username: '', - host: '', - port: 3306, - password: '', - loop: 3, - top_n: 30, - ...initialQueryBaseValues, -}; - -export const initialSwitchValues = { conditions: [] }; - -export const initialWenCaiValues = { - top_n: 20, - query_type: 'stock', - ...initialQueryBaseValues, -}; - -export const initialAkShareValues = { top_n: 10, ...initialQueryBaseValues }; - -export const initialYahooFinanceValues = { - info: true, - history: false, - financials: false, - balance_sheet: false, - cash_flow_statement: false, - news: true, - ...initialQueryBaseValues, -}; - -export const initialJin10Values = { - type: 'flash', - secret_key: 'xxx', - flash_type: '1', - contain: '', - filter: '', - ...initialQueryBaseValues, -}; - -export const initialConcentratorValues = {}; - -export const initialTuShareValues = { - token: 'xxx', - src: 'eastmoney', - start_date: '2024-01-01 09:00:00', - ...initialQueryBaseValues, -}; - -export const initialNoteValues = { - text: '', -}; - -export const initialCrawlerValues = { - extract_type: 'markdown', - ...initialQueryBaseValues, -}; - -export const initialInvokeValues = { - url: 'http://', - method: 'GET', - timeout: 60, - headers: `{ - "Accept": "*/*", - "Cache-Control": "no-cache", - "Connection": "keep-alive" -}`, - proxy: 'http://', - clean_html: false, - datatype: 'json', -}; - -export const initialTemplateValues = { - content: '', - parameters: [], -}; - -export const initialEmailValues = { - smtp_server: '', - smtp_port: 587, - email: '', - password: '', - sender_name: '', - to_email: '', - cc_email: '', - subject: '', - content: '', -}; - -export const initialIterationValues = { - delimiter: ',', -}; -export const initialIterationStartValues = {}; - -export const initialCodeValues = { - lang: 'python', - script: CodeTemplateStrMap[ProgrammingLanguage.Python], - arguments: [ - { - name: 'arg1', - }, - { - name: 'arg2', - }, - ], -}; - -export const CategorizeAnchorPointPositions = [ - { top: 1, right: 34 }, - { top: 8, right: 18 }, - { top: 15, right: 10 }, - { top: 24, right: 4 }, - { top: 31, right: 1 }, - { top: 38, right: -2 }, - { top: 62, right: -2 }, //bottom - { top: 71, right: 1 }, - { top: 79, right: 6 }, - { top: 86, right: 12 }, - { top: 91, right: 20 }, - { top: 98, right: 34 }, -]; - -// key is the source of the edge, value is the target of the edge -// no connection lines are allowed between key and value -export const RestrictedUpstreamMap = { - [Operator.Begin]: [Operator.Relevant], - [Operator.Categorize]: [ - Operator.Begin, - Operator.Categorize, - Operator.Answer, - Operator.Relevant, - ], - [Operator.Answer]: [ - Operator.Begin, - Operator.Answer, - Operator.Message, - Operator.Relevant, - ], - [Operator.Retrieval]: [Operator.Begin, Operator.Retrieval], - [Operator.Generate]: [Operator.Begin, Operator.Relevant], - [Operator.Message]: [ - Operator.Begin, - Operator.Message, - Operator.Generate, - Operator.Retrieval, - Operator.RewriteQuestion, - Operator.Categorize, - Operator.Relevant, - ], - [Operator.Relevant]: [Operator.Begin, Operator.Answer, Operator.Relevant], - [Operator.RewriteQuestion]: [ - Operator.Begin, - Operator.Message, - Operator.RewriteQuestion, - Operator.Relevant, - ], - [Operator.KeywordExtract]: [ - Operator.Begin, - Operator.Message, - Operator.Relevant, - ], - [Operator.Baidu]: [Operator.Begin, Operator.Retrieval], - [Operator.DuckDuckGo]: [Operator.Begin, Operator.Retrieval], - [Operator.Wikipedia]: [Operator.Begin, Operator.Retrieval], - [Operator.PubMed]: [Operator.Begin, Operator.Retrieval], - [Operator.ArXiv]: [Operator.Begin, Operator.Retrieval], - [Operator.Google]: [Operator.Begin, Operator.Retrieval], - [Operator.Bing]: [Operator.Begin, Operator.Retrieval], - [Operator.GoogleScholar]: [Operator.Begin, Operator.Retrieval], - [Operator.DeepL]: [Operator.Begin, Operator.Retrieval], - [Operator.GitHub]: [Operator.Begin, Operator.Retrieval], - [Operator.BaiduFanyi]: [Operator.Begin, Operator.Retrieval], - [Operator.QWeather]: [Operator.Begin, Operator.Retrieval], - [Operator.ExeSQL]: [Operator.Begin], - [Operator.Switch]: [Operator.Begin], - [Operator.WenCai]: [Operator.Begin], - [Operator.AkShare]: [Operator.Begin], - [Operator.YahooFinance]: [Operator.Begin], - [Operator.Jin10]: [Operator.Begin], - [Operator.Concentrator]: [Operator.Begin], - [Operator.TuShare]: [Operator.Begin], - [Operator.Crawler]: [Operator.Begin], - [Operator.Note]: [], - [Operator.Invoke]: [Operator.Begin], - [Operator.Template]: [Operator.Begin, Operator.Relevant], - [Operator.Email]: [Operator.Begin], - [Operator.Iteration]: [Operator.Begin], - [Operator.IterationStart]: [Operator.Begin], - [Operator.Code]: [Operator.Begin], -}; - -export const NodeMap = { - [Operator.Begin]: 'beginNode', - [Operator.Categorize]: 'categorizeNode', - [Operator.Retrieval]: 'retrievalNode', - [Operator.Generate]: 'generateNode', - [Operator.Answer]: 'logicNode', - [Operator.Message]: 'messageNode', - [Operator.Relevant]: 'relevantNode', - [Operator.RewriteQuestion]: 'rewriteNode', - [Operator.KeywordExtract]: 'keywordNode', - [Operator.DuckDuckGo]: 'ragNode', - [Operator.Baidu]: 'ragNode', - [Operator.Wikipedia]: 'ragNode', - [Operator.PubMed]: 'ragNode', - [Operator.ArXiv]: 'ragNode', - [Operator.Google]: 'ragNode', - [Operator.Bing]: 'ragNode', - [Operator.GoogleScholar]: 'ragNode', - [Operator.DeepL]: 'ragNode', - [Operator.GitHub]: 'ragNode', - [Operator.BaiduFanyi]: 'ragNode', - [Operator.QWeather]: 'ragNode', - [Operator.ExeSQL]: 'ragNode', - [Operator.Switch]: 'switchNode', - [Operator.Concentrator]: 'logicNode', - [Operator.WenCai]: 'ragNode', - [Operator.AkShare]: 'ragNode', - [Operator.YahooFinance]: 'ragNode', - [Operator.Jin10]: 'ragNode', - [Operator.TuShare]: 'ragNode', - [Operator.Note]: 'noteNode', - [Operator.Crawler]: 'ragNode', - [Operator.Invoke]: 'invokeNode', - [Operator.Template]: 'templateNode', - [Operator.Email]: 'emailNode', - [Operator.Iteration]: 'group', - [Operator.IterationStart]: 'iterationStartNode', - [Operator.Code]: 'ragNode', -}; - -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', - 'postgres', - 'mariadb', - 'mssql', - 'IBM DB2', -].map((x) => ({ - label: upperFirst(x), - value: x, -})); - -export const SwitchElseTo = 'end_cpn_id'; - -export const SwitchOperatorOptions = [ - { value: '=', label: 'equal' }, - { value: '≠', label: 'notEqual' }, - { value: '>', label: 'gt' }, - { value: '≥', label: 'ge' }, - { value: '<', label: 'lt' }, - { value: '≤', label: 'le' }, - { value: 'contains', label: 'contains' }, - { value: 'not contains', label: 'notContains' }, - { value: 'start with', label: 'startWith' }, - { value: 'end with', label: 'endWith' }, - { value: 'empty', label: 'empty' }, - { value: 'not empty', label: 'notEmpty' }, -]; - -export const SwitchLogicOperatorOptions = ['and', 'or']; - -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']; - -export enum BeginQueryType { - Line = 'line', - Paragraph = 'paragraph', - Options = 'options', - File = 'file', - Integer = 'integer', - Boolean = 'boolean', - KnowledgeBases = 'kb', -} - -export const BeginQueryTypeIconMap = { - [BeginQueryType.Line]: TextCursorInput, - [BeginQueryType.Paragraph]: WrapText, - [BeginQueryType.Options]: OptionIcon, - [BeginQueryType.File]: CloudUpload, - [BeginQueryType.Integer]: ListOrdered, - [BeginQueryType.Boolean]: ToggleLeft, - [BeginQueryType.KnowledgeBases]: Database, -}; - -export const NoDebugOperatorsList = [ - Operator.Begin, - Operator.Answer, - Operator.Concentrator, - Operator.Template, - Operator.Message, - Operator.RewriteQuestion, - Operator.Switch, - Operator.Iteration, -]; diff --git a/web/src/pages/flow/context.ts b/web/src/pages/flow/context.ts deleted file mode 100644 index fe51d8d6d..000000000 --- a/web/src/pages/flow/context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { createContext } from 'react'; - -export const FlowFormContext = createContext<RAGFlowNodeType | undefined>( - undefined, -); diff --git a/web/src/pages/flow/customer_service.json b/web/src/pages/flow/customer_service.json deleted file mode 100644 index 497ee6cda..000000000 --- a/web/src/pages/flow/customer_service.json +++ /dev/null @@ -1,312 +0,0 @@ -{ - "edges": [ - { - "id": "63a2f242-8e71-4098-a46a-459a76d538bd", - "label": "", - "source": "begin", - "target": "answer:0" - }, - { - "id": "cc6dd8bb-e9dc-46e8-9009-5b96f98ae6c0", - "label": "", - "source": "generate:casual", - "target": "answer:0" - }, - { - "id": "58dbf05a-07fc-4a0a-8c03-5f9117e48c35", - "label": "", - "source": "generate:answer", - "target": "answer:0" - }, - { - "id": "dd0ff4f2-4d75-4e7d-a505-3e9533402823", - "label": "", - "source": "generate:complain", - "target": "answer:0" - }, - { - "id": "3dc7a511-9cde-4080-a572-6b06a64e0458", - "label": "", - "source": "generate:ask_contact", - "target": "answer:0" - }, - { - "id": "20e31fee-c392-4257-860a-5844d264198e", - "label": "", - "source": "message:get_contact", - "target": "answer:0" - }, - { - "id": "104ac8bb-5d75-4eca-8065-1d8fc2805b8e", - "label": "", - "source": "answer:0", - "target": "categorize:0" - }, - { - "id": "755864b5-9eef-44ec-a560-9a23b5a3f9ee", - "label": "", - "source": "categorize:0", - "target": "retrieval:0", - "sourceHandle": "product_related" - }, - { - "id": "7f68384d-3441-4bfa-bf13-69af67e857d2", - "label": "", - "source": "categorize:0", - "target": "generate:casual", - "sourceHandle": "casual" - }, - { - "id": "c9bf8e81-9345-4885-b565-be2f5b16f6ef", - "label": "", - "source": "categorize:0", - "target": "generate:complain", - "sourceHandle": "complain" - }, - { - "id": "2f326699-621b-4d28-ab98-70d99ad21add", - "label": "", - "source": "categorize:0", - "target": "message:get_contact", - "sourceHandle": "answer" - }, - { - "id": "03e45174-55df-47d6-8e5f-fbe2ffc148d7", - "label": "", - "source": "retrieval:0", - "target": "relevant:0" - }, - { - "id": "a26027ac-e8a9-48e4-814c-3262b8d81913", - "label": "", - "source": "relevant:0", - "target": "generate:answer" - }, - { - "id": "04d99dc7-6120-4169-98c6-7bc2813aa85b", - "label": "", - "source": "relevant:0", - "target": "generate:ask_contact" - } - ], - "nodes": [ - { - "id": "begin", - "type": "beginNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Begin", - "name": "LegalPoetsAttack", - "form": { - "prologue": "Hi! How can I help you?" - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "answer:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Answer", - "name": "ThreeGeeseBehave", - "form": {} - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "categorize:0", - "type": "categorizeNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Categorize", - "name": "PublicComicsHammer", - "form": { - "llm_id": "deepseek-chat", - "category_description": { - "product_related": { - "description": "The question is about the product usage, appearance and how it works.", - "examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?\nException: Can't connect to ES cluster\nHow to build the RAGFlow image from scratch", - "to": "retrieval:0" - }, - "casual": { - "description": "The question is not about the product usage, appearance and how it works. Just casual chat.", - "examples": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?", - "to": "generate:casual" - }, - "complain": { - "description": "Complain even curse about the product or service you provide. But the comment is not specific enough.", - "examples": "How bad is it.\nIt's really sucks.\nDamn, for God's sake, can it be more steady?\nShit, I just can't use this shit.\nI can't stand it anymore.", - "to": "generate:complain" - }, - "answer": { - "description": "This answer provide a specific contact information, like e-mail, phone number, wechat number, line number, twitter, discord, etc,.", - "examples": "My phone number is 203921\nkevinhu.hk@gmail.com\nThis is my discord number: johndowson_29384", - "to": "message:get_contact" - } - }, - "message_history_window_size": 8 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:casual", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "SourKnivesPay", - "form": { - "llm_id": "deepseek-chat", - "prompt": "You are a customer support. But the customer wants to have a casual chat with you instead of consulting about the product. Be nice, funny, enthusiasm and concern.", - "temperature": 0.9, - "message_history_window_size": 12, - "cite": false - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:complain", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "TameLlamasSniff", - "form": { - "llm_id": "deepseek-chat", - "prompt": "You are a customer support. the Customers complain even curse about the products but not specific enough. You need to ask him/her what's the specific problem with the product. Be nice, patient and concern to soothe your customers’ emotions at first place.", - "temperature": 0.9, - "message_history_window_size": 12, - "cite": false - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "retrieval:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Retrieval", - "name": "ShinyPathsDraw", - "form": { - "similarity_threshold": 0.2, - "keywords_similarity_weight": 0.3, - "top_n": 6, - "top_k": 1024, - "rerank_id": "BAAI/bge-reranker-v2-m3", - "kb_ids": ["869a236818b811ef91dffa163e197198"] - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "relevant:0", - "type": "relevantNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Relevant", - "name": "LegalPotsLick", - "form": { - "llm_id": "deepseek-chat", - "temperature": 0.02, - "yes": "generate:answer", - "no": "generate:ask_contact" - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:answer", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "YellowGamesReport", - "form": { - "llm_id": "deepseek-chat", - "prompt": "You are an intelligent assistant. Please answer the question based on content of knowledge base. When all knowledge base content is irrelevant to the question, your answer must include the sentence \"The answer you are looking for is not found in the knowledge base!\". Answers need to consider chat history.\n Knowledge base content is as following:\n {input}\n The above is the content of knowledge base.", - "temperature": 0.02 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:ask_contact", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "FamousChefsRetire", - "form": { - "llm_id": "deepseek-chat", - "prompt": "You are a customer support. But you can't answer to customers' question. You need to request their contact like E-mail, phone number, Wechat number, LINE number, twitter, discord, etc,. Product experts will contact them later. Please do not ask the same question twice.", - "temperature": 0.9, - "message_history_window_size": 12, - "cite": false - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "message:get_contact", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Message", - "name": "BlueBooksTan", - "form": { - "messages": [ - "Okay, I've already write this down. What else I can do for you?", - "Get it. What else I can do for you?", - "Thanks for your trust! Our expert will contact ASAP. So, anything else I can do for you?", - "Thanks! So, anything else I can do for you?" - ] - } - }, - "sourcePosition": "left", - "targetPosition": "right" - } - ] -} diff --git a/web/src/pages/flow/debug-content/index.less b/web/src/pages/flow/debug-content/index.less deleted file mode 100644 index fda707810..000000000 --- a/web/src/pages/flow/debug-content/index.less +++ /dev/null @@ -1,5 +0,0 @@ -.formWrapper { - :global(.ant-form-item-label) { - font-weight: 600 !important; - } -} diff --git a/web/src/pages/flow/debug-content/index.tsx b/web/src/pages/flow/debug-content/index.tsx deleted file mode 100644 index 6fcd4e9a7..000000000 --- a/web/src/pages/flow/debug-content/index.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { Authorization } from '@/constants/authorization'; -import { useSetModalState } from '@/hooks/common-hooks'; -import { useSetSelectedRecord } from '@/hooks/logic-hooks'; -import { useHandleSubmittable } from '@/hooks/login-hooks'; -import api from '@/utils/api'; -import { getAuthorization } from '@/utils/authorization-util'; -import { UploadOutlined } from '@ant-design/icons'; -import { - Button, - Form, - FormItemProps, - Input, - InputNumber, - Select, - Switch, - Upload, -} from 'antd'; -import { UploadChangeParam, UploadFile } from 'antd/es/upload'; -import { pick } from 'lodash'; -import { Link } from 'lucide-react'; -import React, { useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BeginQueryType } from '../constant'; -import { BeginQuery } from '../interface'; -import { PopoverForm } from './popover-form'; - -import styles from './index.less'; -import KnowledgeBaseItem from '@/components/knowledge-base-item'; - -interface IProps { - parameters: BeginQuery[]; - ok(parameters: any[]): void; - isNext?: boolean; - loading?: boolean; - submitButtonDisabled?: boolean; -} - -const DebugContent = ({ - parameters, - ok, - isNext = true, - loading = false, - submitButtonDisabled = false, -}: IProps) => { - const { t } = useTranslation(); - const [form] = Form.useForm(); - const { - visible, - hideModal: hidePopover, - switchVisible, - showModal: showPopover, - } = useSetModalState(); - const { setRecord, currentRecord } = useSetSelectedRecord<number>(); - const { submittable } = useHandleSubmittable(form); - const [isUploading, setIsUploading] = useState(false); - - const handleShowPopover = useCallback( - (idx: number) => () => { - setRecord(idx); - showPopover(); - }, - [setRecord, showPopover], - ); - - const normFile = (e: any) => { - if (Array.isArray(e)) { - return e; - } - return e?.fileList; - }; - - const onChange = useCallback( - (optional: boolean) => - ({ fileList }: UploadChangeParam<UploadFile>) => { - if (!optional) { - setIsUploading(fileList.some((x) => x.status === 'uploading')); - } - }, - [], - ); - - const renderWidget = useCallback( - (q: BeginQuery, idx: number) => { - const props: FormItemProps & { key: number } = { - key: idx, - label: q.name ?? q.key, - name: idx, - }; - if (q.optional === false) { - props.rules = [{ required: true }]; - } - - const urlList: { url: string; result: string }[] = - form.getFieldValue(idx) || []; - - const BeginQueryTypeMap = { - [BeginQueryType.Line]: ( - <Form.Item {...props}> - <Input></Input> - </Form.Item> - ), - [BeginQueryType.Paragraph]: ( - <Form.Item {...props}> - <Input.TextArea rows={1}></Input.TextArea> - </Form.Item> - ), - [BeginQueryType.Options]: ( - <Form.Item {...props}> - <Select - allowClear - options={q.options?.map((x) => ({ label: x, value: x })) ?? []} - ></Select> - </Form.Item> - ), - [BeginQueryType.File]: ( - <React.Fragment key={idx}> - <Form.Item label={q.name ?? q.key} required={!q.optional}> - <div className="relative"> - <Form.Item - {...props} - valuePropName="fileList" - getValueFromEvent={normFile} - noStyle - > - <Upload - name="file" - action={api.parse} - multiple - headers={{ [Authorization]: getAuthorization() }} - onChange={onChange(q.optional)} - > - <Button icon={<UploadOutlined />}> - {t('common.upload')} - </Button> - </Upload> - </Form.Item> - <Form.Item - {...pick(props, ['key', 'label', 'rules'])} - required={!q.optional} - className={urlList.length > 0 ? 'mb-1' : ''} - noStyle - > - <PopoverForm visible={visible} switchVisible={switchVisible}> - <Button - onClick={handleShowPopover(idx)} - className="absolute left-1/2 top-0" - icon={<Link className="size-3" />} - > - {t('flow.pasteFileLink')} - </Button> - </PopoverForm> - </Form.Item> - </div> - </Form.Item> - <Form.Item name={idx} noStyle {...pick(props, ['rules'])} /> - </React.Fragment> - ), - [BeginQueryType.Integer]: ( - <Form.Item {...props}> - <InputNumber></InputNumber> - </Form.Item> - ), - [BeginQueryType.Boolean]: ( - <Form.Item valuePropName={'checked'} {...props}> - <Switch></Switch> - </Form.Item> - ), - [BeginQueryType.KnowledgeBases]: ( - <KnowledgeBaseItem - name={idx.toString()} - label={q.name || q.key} - required={!q.optional} - ></KnowledgeBaseItem> - ) - }; - - return ( - BeginQueryTypeMap[q.type as BeginQueryType] ?? - BeginQueryTypeMap[BeginQueryType.Paragraph] - ); - }, - [form, handleShowPopover, onChange, switchVisible, t, visible], - ); - - const onOk = useCallback(async () => { - const values = await form.validateFields(); - const nextValues = Object.entries(values).map(([key, value]) => { - const item = parameters[Number(key)]; - let nextValue = value; - if (Array.isArray(value)) { - nextValue = ``; - - if (item.type === 'kb') { - nextValue = value.join(',') - } else { - value.forEach((x) => { - nextValue += - x?.originFileObj instanceof File - ? `${x.name}\n${x.response?.data}\n----\n` - : `${x.url}\n${x.result}\n----\n`; - }); - } - } - return { ...item, value: nextValue }; - }); - - ok(nextValues); - }, [form, ok, parameters]); - - return ( - <> - <section className={styles.formWrapper}> - <Form.Provider - onFormFinish={(name, { values, forms }) => { - if (name === 'urlForm') { - const { basicForm } = forms; - const urlInfo = basicForm.getFieldValue(currentRecord) || []; - basicForm.setFieldsValue({ - [currentRecord]: [...urlInfo, { ...values, name: values.url }], - }); - hidePopover(); - } - }} - > - <Form - name="basicForm" - autoComplete="off" - layout={'vertical'} - form={form} - > - {parameters.map((x, idx) => { - return renderWidget(x, idx); - })} - </Form> - </Form.Provider> - </section> - <Button - type={'primary'} - block - onClick={onOk} - loading={loading} - disabled={!submittable || isUploading || submitButtonDisabled} - > - {t(isNext ? 'common.next' : 'flow.run')} - </Button> - </> - ); -}; - -export default DebugContent; diff --git a/web/src/pages/flow/debug-content/popover-form.tsx b/web/src/pages/flow/debug-content/popover-form.tsx deleted file mode 100644 index 557e3185b..000000000 --- a/web/src/pages/flow/debug-content/popover-form.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useParseDocument } from '@/hooks/document-hooks'; -import { useResetFormOnCloseModal } from '@/hooks/logic-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { Button, Form, Input, Popover } from 'antd'; -import { PropsWithChildren } from 'react'; -import { useTranslation } from 'react-i18next'; - -const reg = - /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/; - -export const PopoverForm = ({ - children, - visible, - switchVisible, -}: PropsWithChildren<IModalProps<any>>) => { - const [form] = Form.useForm(); - const { parseDocument, loading } = useParseDocument(); - const { t } = useTranslation(); - - useResetFormOnCloseModal({ - form, - visible, - }); - - const onOk = async () => { - const values = await form.validateFields(); - const val = values.url; - - if (reg.test(val)) { - const ret = await parseDocument(val); - if (ret?.data?.code === 0) { - form.setFieldValue('result', ret?.data?.data); - form.submit(); - } - } - }; - - const content = ( - <Form form={form} name="urlForm"> - <Form.Item - name="url" - rules={[{ required: true, type: 'url' }]} - className="m-0" - > - <Input - onPressEnter={(e) => e.preventDefault()} - placeholder={t('flow.pasteFileLink')} - suffix={ - <Button - type="primary" - onClick={onOk} - size={'small'} - loading={loading} - > - {t('common.submit')} - </Button> - } - /> - </Form.Item> - <Form.Item name={'result'} noStyle /> - </Form> - ); - - return ( - <Popover - content={content} - open={visible} - trigger={'click'} - onOpenChange={switchVisible} - > - {children} - </Popover> - ); -}; diff --git a/web/src/pages/flow/flow-drawer/index.less b/web/src/pages/flow/flow-drawer/index.less deleted file mode 100644 index 1348cc66d..000000000 --- a/web/src/pages/flow/flow-drawer/index.less +++ /dev/null @@ -1,21 +0,0 @@ -.title { - flex-basis: 60px; -} - -.formWrapper { - :global(.ant-form-item-label) { - font-weight: 600; - } -} - -.operatorDescription { - font-size: 14px; - padding-top: 16px; - font-weight: normal; -} - -.formDrawer { - :global(.ant-drawer-content-wrapper) { - transform: translateX(0) !important; - } -} diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx deleted file mode 100644 index e9778f1b5..000000000 --- a/web/src/pages/flow/flow-drawer/index.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { CloseOutlined } from '@ant-design/icons'; -import { Drawer, Flex, Form, Input } from 'antd'; -import { get, isPlainObject, lowerFirst } from 'lodash'; -import { Play } from 'lucide-react'; -import { useEffect, useRef } from 'react'; -import { BeginId, Operator, operatorMap } from '../constant'; -import AkShareForm from '../form/akshare-form'; -import AnswerForm from '../form/answer-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 GenerateForm from '../form/generate-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 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'; -import RewriteQuestionForm from '../form/rewrite-question-form'; -import SwitchForm from '../form/switch-form'; -import TemplateForm from '../form/template-form'; -import TuShareForm from '../form/tushare-form'; -import WenCaiForm from '../form/wencai-form'; -import WikipediaForm from '../form/wikipedia-form'; -import YahooFinanceForm from '../form/yahoo-finance-form'; -import { useHandleFormValuesChange, useHandleNodeNameChange } from '../hooks'; -import OperatorIcon from '../operator-icon'; -import { - buildCategorizeListFromObject, - getDrawerWidth, - needsSingleStepDebugging, -} from '../utils'; -import SingleDebugDrawer from './single-debug-drawer'; - -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { FlowFormContext } from '../context'; -import { RunTooltip } from '../flow-tooltip'; -import IterationForm from '../form/iteration-from'; -import styles from './index.less'; - -interface IProps { - node?: RAGFlowNodeType; - singleDebugDrawerVisible: IModalProps<any>['visible']; - hideSingleDebugDrawer: IModalProps<any>['hideModal']; - showSingleDebugDrawer: IModalProps<any>['showModal']; -} - -const FormMap = { - [Operator.Begin]: BeginForm, - [Operator.Retrieval]: RetrievalForm, - [Operator.Generate]: GenerateForm, - [Operator.Answer]: AnswerForm, - [Operator.Categorize]: CategorizeForm, - [Operator.Message]: MessageForm, - [Operator.Relevant]: RelevantForm, - [Operator.RewriteQuestion]: RewriteQuestionForm, - [Operator.Baidu]: BaiduForm, - [Operator.DuckDuckGo]: DuckDuckGoForm, - [Operator.KeywordExtract]: KeywordExtractForm, - [Operator.Wikipedia]: WikipediaForm, - [Operator.PubMed]: PubMedForm, - [Operator.ArXiv]: ArXivForm, - [Operator.Google]: GoogleForm, - [Operator.Bing]: BingForm, - [Operator.GoogleScholar]: GoogleScholarForm, - [Operator.DeepL]: DeepLForm, - [Operator.GitHub]: GithubForm, - [Operator.BaiduFanyi]: BaiduFanyiForm, - [Operator.QWeather]: QWeatherForm, - [Operator.ExeSQL]: ExeSQLForm, - [Operator.Switch]: SwitchForm, - [Operator.WenCai]: WenCaiForm, - [Operator.AkShare]: AkShareForm, - [Operator.YahooFinance]: YahooFinanceForm, - [Operator.Jin10]: Jin10Form, - [Operator.TuShare]: TuShareForm, - [Operator.Crawler]: CrawlerForm, - [Operator.Invoke]: InvokeForm, - [Operator.Concentrator]: () => <></>, - [Operator.Note]: () => <></>, - [Operator.Template]: TemplateForm, - [Operator.Email]: EmailForm, - [Operator.Iteration]: IterationForm, - [Operator.IterationStart]: () => <></>, - [Operator.Code]: CodeForm, -}; - -const EmptyContent = () => <div></div>; - -const FormDrawer = ({ - visible, - hideModal, - node, - singleDebugDrawerVisible, - hideSingleDebugDrawer, - showSingleDebugDrawer, -}: IModalProps<any> & IProps) => { - const operatorName: Operator = node?.data.label as Operator; - const OperatorForm = FormMap[operatorName] ?? EmptyContent; - const [form] = Form.useForm(); - const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({ - id: node?.id, - data: node?.data, - }); - const previousId = useRef<string | undefined>(node?.id); - - const { t } = useTranslate('flow'); - - const { handleValuesChange } = useHandleFormValuesChange(node?.id); - - useEffect(() => { - if (visible) { - if (node?.id !== previousId.current) { - form.resetFields(); - } - - if (operatorName === Operator.Categorize) { - const items = buildCategorizeListFromObject( - get(node, 'data.form.category_description', {}), - ); - const formData = node?.data?.form; - if (isPlainObject(formData)) { - form.setFieldsValue({ ...formData, items }); - } - } else { - form.setFieldsValue(node?.data?.form); - } - previousId.current = node?.id; - } - }, [visible, form, node?.data?.form, node?.id, node, operatorName]); - - return ( - <Drawer - title={ - <Flex vertical> - <Flex gap={'middle'} align="center"> - <OperatorIcon - name={operatorName} - color={operatorMap[operatorName]?.color} - ></OperatorIcon> - <Flex align="center" gap={'small'} flex={1}> - <label htmlFor="" className={styles.title}> - {t('title')} - </label> - {node?.id === BeginId ? ( - <span>{t(BeginId)}</span> - ) : ( - <Input - value={name} - onBlur={handleNameBlur} - onChange={handleNameChange} - ></Input> - )} - </Flex> - - {needsSingleStepDebugging(operatorName) && ( - <RunTooltip> - <Play - className="size-5 cursor-pointer" - onClick={showSingleDebugDrawer} - /> - </RunTooltip> - )} - <CloseOutlined onClick={hideModal} /> - </Flex> - <span className={styles.operatorDescription}> - {t(`${lowerFirst(operatorName)}Description`)} - </span> - </Flex> - } - placement="right" - onClose={hideModal} - open={visible} - getContainer={false} - mask={false} - width={getDrawerWidth()} - closeIcon={null} - rootClassName={styles.formDrawer} - > - <section className={styles.formWrapper}> - {visible && ( - <FlowFormContext.Provider value={node}> - <OperatorForm - onValuesChange={handleValuesChange} - form={form} - node={node} - ></OperatorForm> - </FlowFormContext.Provider> - )} - </section> - {singleDebugDrawerVisible && ( - <SingleDebugDrawer - visible={singleDebugDrawerVisible} - hideModal={hideSingleDebugDrawer} - componentId={node?.id} - ></SingleDebugDrawer> - )} - </Drawer> - ); -}; - -export default FormDrawer; diff --git a/web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx b/web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx deleted file mode 100644 index f9908ef7e..000000000 --- a/web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import CopyToClipboard from '@/components/copy-to-clipboard'; -import { useDebugSingle, useFetchInputElements } from '@/hooks/flow-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { CloseOutlined } from '@ant-design/icons'; -import { Drawer } from 'antd'; -import { isEmpty } from 'lodash'; -import { useCallback } 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'; - -interface IProps { - componentId?: string; -} - -const SingleDebugDrawer = ({ - componentId, - visible, - hideModal, -}: IModalProps<any> & IProps) => { - const { t } = useTranslation(); - const { data: list } = useFetchInputElements(componentId); - const { debugSingle, data, loading } = useDebugSingle(); - - const onOk = useCallback( - (nextValues: any[]) => { - if (componentId) { - debugSingle({ component_id: componentId, params: nextValues }); - } - }, - [componentId, debugSingle], - ); - - const content = JSON.stringify(data, null, 2); - - return ( - <Drawer - title={ - <div className="flex justify-between"> - {t('flow.testRun')} - <CloseOutlined onClick={hideModal} /> - </div> - } - width={'100%'} - onClose={hideModal} - open={visible} - getContainer={false} - mask={false} - placement={'bottom'} - height={'95%'} - closeIcon={null} - > - <section className="overflow-y-auto"> - <DebugContent - parameters={list} - ok={onOk} - isNext={false} - loading={loading} - submitButtonDisabled={list.length === 0} - ></DebugContent> - {!isEmpty(data) ? ( - <div className="mt-4 rounded-md bg-slate-200 border border-neutral-200"> - <div className="flex justify-between p-2"> - <span>JSON</span> - <CopyToClipboard text={content}></CopyToClipboard> - </div> - <JsonView - src={data} - displaySize - collapseStringsAfterLength={100000000000} - className="w-full h-[800px] break-words overflow-auto p-2 bg-slate-100" - /> - </div> - ) : null} - </section> - </Drawer> - ); -}; - -export default SingleDebugDrawer; diff --git a/web/src/pages/flow/flow-id-modal/index.less b/web/src/pages/flow/flow-id-modal/index.less deleted file mode 100644 index c95b34c95..000000000 --- a/web/src/pages/flow/flow-id-modal/index.less +++ /dev/null @@ -1,3 +0,0 @@ -.id { - .linkText(); -} diff --git a/web/src/pages/flow/flow-id-modal/index.tsx b/web/src/pages/flow/flow-id-modal/index.tsx deleted file mode 100644 index 7c54a3c44..000000000 --- a/web/src/pages/flow/flow-id-modal/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { Modal, Typography } from 'antd'; - -import { useParams } from 'umi'; -import styles from './index.less'; - -const { Paragraph, Link } = Typography; - -const FlowIdModal = ({ hideModal }: IModalProps<any>) => { - const { t } = useTranslate('flow'); - const { id } = useParams(); - - return ( - <Modal - title={'Agent ID'} - open - onCancel={hideModal} - cancelButtonProps={{ style: { display: 'none' } }} - onOk={hideModal} - okText={t('close', { keyPrefix: 'common' })} - > - <Paragraph copyable={{ text: id }} className={styles.id}> - {id} - </Paragraph> - <Link - href="https://ragflow.io/docs/dev/http_api_reference#create-session-with-an-agent" - target="_blank" - > - {t('howUseId')} - </Link> - </Modal> - ); -}; - -export default FlowIdModal; diff --git a/web/src/pages/flow/flow-setting/index.less b/web/src/pages/flow/flow-setting/index.less deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/src/pages/flow/flow-setting/index.tsx b/web/src/pages/flow/flow-setting/index.tsx deleted file mode 100644 index d371e3f40..000000000 --- a/web/src/pages/flow/flow-setting/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { useFetchFlow, useSettingFlow } from '@/hooks/flow-hooks'; -import { normFile } from '@/utils/file-util'; -import { PlusOutlined } from '@ant-design/icons'; -import { Form, Input, Modal, Radio, Upload } from 'antd'; -import React, { useCallback, useEffect } from 'react'; -export function useFlowSettingModal() { - const [visibleSettingModal, setVisibleSettingMModal] = React.useState(false); - - return { - visibleSettingModal, - setVisibleSettingMModal, - }; -} - -type FlowSettingModalProps = { - visible: boolean; - hideModal: () => void; - id: string; -}; -export const FlowSettingModal = ({ - hideModal, - visible, - id, -}: FlowSettingModalProps) => { - const { data, refetch } = useFetchFlow(); - const [form] = Form.useForm(); - const { t } = useTranslate('flow.settings'); - const { loading, settingFlow } = useSettingFlow(); - // Initialize form with data when it becomes available - useEffect(() => { - if (data) { - form.setFieldsValue({ - title: data.title, - description: data.description, - permission: data.permission, - avatar: data.avatar ? [{ thumbUrl: data.avatar }] : [], - }); - } - }, [data, form]); - - const handleSubmit = useCallback(async () => { - if (!id) return; - try { - const { avatar, ...others } = await form.validateFields(); - const param = { - ...others, - id, - avatar: avatar && avatar.length > 0 ? avatar[0].thumbUrl : '', - }; - settingFlow(param); - } catch (error) { - console.error('Validation failed:', error); - } - }, [form, id, settingFlow]); - React.useEffect(() => { - if (!loading && refetch && visible) { - refetch(); - } - }, [loading, refetch, visible]); - return ( - <Modal - confirmLoading={loading} - title={t('agentSetting')} - open={visible} - onCancel={hideModal} - onOk={handleSubmit} - okText={t('save', { keyPrefix: 'common' })} - cancelText={t('cancel', { keyPrefix: 'common' })} - > - <Form - form={form} - labelCol={{ span: 6 }} - wrapperCol={{ span: 18 }} - layout="horizontal" - style={{ maxWidth: 600 }} - > - <Form.Item - name="title" - label={t('title')} - rules={[{ required: true, message: 'Please input a title!' }]} - > - <Input /> - </Form.Item> - <Form.Item - name="avatar" - label={t('photo')} - valuePropName="fileList" - getValueFromEvent={normFile} - > - <Upload - listType="picture-card" - maxCount={1} - beforeUpload={() => false} - showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }} - > - <button style={{ border: 0, background: 'none' }} type="button"> - <PlusOutlined /> - <div style={{ marginTop: 8 }}>{t('upload')}</div> - </button> - </Upload> - </Form.Item> - <Form.Item name="description" label={t('description')}> - <Input.TextArea rows={4} /> - </Form.Item> - - <Form.Item - name="permission" - label={t('permissions')} - tooltip={t('permissionsTip')} - rules={[{ required: true }]} - > - <Radio.Group> - <Radio value="me">{t('me')}</Radio> - <Radio value="team">{t('team')}</Radio> - </Radio.Group> - </Form.Item> - </Form> - </Modal> - ); -}; diff --git a/web/src/pages/flow/flow-sider/index.less b/web/src/pages/flow/flow-sider/index.less deleted file mode 100644 index 52d09a15c..000000000 --- a/web/src/pages/flow/flow-sider/index.less +++ /dev/null @@ -1,16 +0,0 @@ -.operatorCard { - :global(.ant-card-body) { - padding: 10px; - } - .cubeIcon { - &:hover { - cursor: pointer; - } - } -} - -.siderContent { - padding: 10px 4px; - overflow: auto; - height: calc(100vh - 80px); -} diff --git a/web/src/pages/flow/flow-sider/index.tsx b/web/src/pages/flow/flow-sider/index.tsx deleted file mode 100644 index b8e888b70..000000000 --- a/web/src/pages/flow/flow-sider/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Card, Divider, Flex, Layout, Tooltip } from 'antd'; -import classNames from 'classnames'; -import lowerFirst from 'lodash/lowerFirst'; -import React from 'react'; -import { Operator, componentMenuList, operatorMap } from '../constant'; -import { useHandleDrag } from '../hooks'; -import OperatorIcon from '../operator-icon'; -import styles from './index.less'; - -const { Sider } = Layout; - -interface IProps { - setCollapsed: (width: boolean) => void; - collapsed: boolean; -} - -const dividerProps = { - marginTop: 10, - marginBottom: 10, - padding: 0, - borderBlockColor: '#b4afaf', - borderStyle: 'dotted', -}; - -const FlowSide = ({ setCollapsed, collapsed }: IProps) => { - const { handleDragStart } = useHandleDrag(); - const { t } = useTranslate('flow'); - - return ( - <Sider - collapsible - collapsed={collapsed} - collapsedWidth={0} - theme={'light'} - onCollapse={(value) => setCollapsed(value)} - > - <Flex vertical gap={10} className={styles.siderContent}> - {componentMenuList.map((x) => { - return ( - <React.Fragment key={x.name}> - {x.name === Operator.Note && ( - <Divider style={dividerProps}></Divider> - )} - {x.name === Operator.DuckDuckGo && ( - <Divider style={dividerProps}></Divider> - )} - <Card - key={x.name} - hoverable - draggable - className={classNames(styles.operatorCard)} - onDragStart={handleDragStart(x.name)} - > - <Flex align="center" gap={15}> - <OperatorIcon - name={x.name} - color={operatorMap[x.name].color} - ></OperatorIcon> - <section> - <Tooltip title={t(`${lowerFirst(x.name)}Description`)}> - <b>{t(lowerFirst(x.name))}</b> - </Tooltip> - </section> - </Flex> - </Card> - </React.Fragment> - ); - })} - </Flex> - </Sider> - ); -}; - -export default FlowSide; diff --git a/web/src/pages/flow/flow-tooltip.tsx b/web/src/pages/flow/flow-tooltip.tsx deleted file mode 100644 index 9386dd06b..000000000 --- a/web/src/pages/flow/flow-tooltip.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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 ( - <Tooltip> - <TooltipTrigger>{children}</TooltipTrigger> - <TooltipContent> - <p>{t('flow.testRun')}</p> - </TooltipContent> - </Tooltip> - ); -}; diff --git a/web/src/pages/flow/form-hooks.ts b/web/src/pages/flow/form-hooks.ts deleted file mode 100644 index 372288e6f..000000000 --- a/web/src/pages/flow/form-hooks.ts +++ /dev/null @@ -1,77 +0,0 @@ -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; -}; - -/** - * dumped - * @param nodeId - * @returns - */ -export const useHandleFormSelectChange = (nodeId?: string) => { - const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore( - (state) => state, - ); - const handleSelectChange = useCallback( - (name?: string) => (value?: string) => { - if (nodeId && name) { - if (value) { - addEdge({ - source: nodeId, - target: value, - sourceHandle: name, - targetHandle: null, - }); - } else { - // clear selected value - deleteEdgeBySourceAndSourceHandle({ - source: nodeId, - sourceHandle: name, - }); - } - } - }, - [addEdge, nodeId, deleteEdgeBySourceAndSourceHandle], - ); - - return { handleSelectChange }; -}; - -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/flow/form/akshare-form/index.tsx b/web/src/pages/flow/form/akshare-form/index.tsx deleted file mode 100644 index 1f7ce99f1..000000000 --- a/web/src/pages/flow/form/akshare-form/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { Form } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const AkShareForm = ({ onValuesChange, form, node }: IOperatorForm) => { - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10} max={99}></TopNItem> - </Form> - ); -}; - -export default AkShareForm; diff --git a/web/src/pages/flow/form/answer-form/index.tsx b/web/src/pages/flow/form/answer-form/index.tsx deleted file mode 100644 index 8db015c05..000000000 --- a/web/src/pages/flow/form/answer-form/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const AnswerForm = () => { - return <div></div>; -}; - -export default AnswerForm; diff --git a/web/src/pages/flow/form/arxiv-form/index.tsx b/web/src/pages/flow/form/arxiv-form/index.tsx deleted file mode 100644 index a44592148..000000000 --- a/web/src/pages/flow/form/arxiv-form/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { useMemo } from 'react'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const ArXivForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const options = useMemo(() => { - return ['submittedDate', 'lastUpdatedDate', 'relevance'].map((x) => ({ - value: x, - label: t(x), - })); - }, [t]); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - - <TopNItem initialValue={10}></TopNItem> - <Form.Item label={t('sortBy')} name={'sort_by'}> - <Select options={options}></Select> - </Form.Item> - </Form> - ); -}; - -export default ArXivForm; diff --git a/web/src/pages/flow/form/baidu-fanyi-form/index.tsx b/web/src/pages/flow/form/baidu-fanyi-form/index.tsx deleted file mode 100644 index c4b399026..000000000 --- a/web/src/pages/flow/form/baidu-fanyi-form/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { useMemo } from 'react'; -import { - BaiduFanyiDomainOptions, - BaiduFanyiSourceLangOptions, -} from '../../constant'; -import { IOperatorForm } from '../../interface'; -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 ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item label={t('appid')} name={'appid'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('secretKey')} name={'secret_key'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('transType')} name={'trans_type'}> - <Select options={options}></Select> - </Form.Item> - <Form.Item noStyle dependencies={['model_type']}> - {({ getFieldValue }) => - getFieldValue('trans_type') === 'fieldtranslate' && ( - <Form.Item label={t('domain')} name={'domain'}> - <Select options={baiduFanyiOptions}></Select> - </Form.Item> - ) - } - </Form.Item> - <Form.Item label={t('sourceLang')} name={'source_lang'}> - <Select options={baiduFanyiSourceLangOptions}></Select> - </Form.Item> - <Form.Item label={t('targetLang')} name={'target_lang'}> - <Select options={baiduFanyiSourceLangOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default BaiduFanyiForm; diff --git a/web/src/pages/flow/form/baidu-form/index.tsx b/web/src/pages/flow/form/baidu-form/index.tsx deleted file mode 100644 index 0c866e488..000000000 --- a/web/src/pages/flow/form/baidu-form/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { Form } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const BaiduForm = ({ onValuesChange, form, node }: IOperatorForm) => { - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10}></TopNItem> - </Form> - ); -}; - -export default BaiduForm; diff --git a/web/src/pages/flow/form/begin-form/begin-dynamic-options.tsx b/web/src/pages/flow/form/begin-form/begin-dynamic-options.tsx deleted file mode 100644 index bcc9c5788..000000000 --- a/web/src/pages/flow/form/begin-form/begin-dynamic-options.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Form, Input } from 'antd'; - -const BeginDynamicOptions = () => { - return ( - <Form.List - name="options" - rules={[ - { - validator: async (_, names) => { - if (!names || names.length < 1) { - return Promise.reject(new Error('At least 1 option')); - } - }, - }, - ]} - > - {(fields, { add, remove }, { errors }) => ( - <> - {fields.map((field, index) => ( - <Form.Item - label={index === 0 ? 'Options' : ''} - required={false} - key={field.key} - > - <Form.Item - {...field} - validateTrigger={['onChange', 'onBlur']} - rules={[ - { - required: true, - whitespace: true, - message: 'Please input option or delete this field.', - }, - ]} - noStyle - > - <Input - placeholder="option" - style={{ width: '90%', marginRight: 16 }} - /> - </Form.Item> - {fields.length > 1 ? ( - <MinusCircleOutlined - className="dynamic-delete-button" - onClick={() => remove(field.name)} - /> - ) : null} - </Form.Item> - ))} - <Form.Item> - <Button - type="dashed" - onClick={() => add()} - icon={<PlusOutlined />} - block - > - Add option - </Button> - <Form.ErrorList errors={errors} /> - </Form.Item> - </> - )} - </Form.List> - ); -}; - -export default BeginDynamicOptions; diff --git a/web/src/pages/flow/form/begin-form/hooks.ts b/web/src/pages/flow/form/begin-form/hooks.ts deleted file mode 100644 index b045f5dc5..000000000 --- a/web/src/pages/flow/form/begin-form/hooks.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useSetModalState } from '@/hooks/common-hooks'; -import { useSetSelectedRecord } from '@/hooks/logic-hooks'; -import { useCallback, useMemo, useState } from 'react'; -import { BeginQuery, IOperatorForm } from '../../interface'; - -export const useEditQueryRecord = ({ form, onValuesChange }: IOperatorForm) => { - const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>(); - const { visible, hideModal, showModal } = useSetModalState(); - const [index, setIndex] = useState(-1); - - const otherThanCurrentQuery = useMemo(() => { - const query: BeginQuery[] = form?.getFieldValue('query') || []; - return query.filter((item, idx) => idx !== index); - }, [form, index]); - - const handleEditRecord = useCallback( - (record: BeginQuery) => { - const query: BeginQuery[] = form?.getFieldValue('query') || []; - - const nextQuery: BeginQuery[] = - index > -1 ? query.toSpliced(index, 1, record) : [...query, record]; - - onValuesChange?.( - { query: nextQuery }, - { query: nextQuery, prologue: form?.getFieldValue('prologue') }, - ); - hideModal(); - }, - [form, hideModal, index, onValuesChange], - ); - - const handleShowModal = useCallback( - (idx?: number, record?: BeginQuery) => { - setIndex(idx ?? -1); - setRecord(record ?? ({} as BeginQuery)); - showModal(); - }, - [setRecord, showModal], - ); - - return { - ok: handleEditRecord, - currentRecord, - setRecord, - visible, - hideModal, - showModal: handleShowModal, - otherThanCurrentQuery, - }; -}; diff --git a/web/src/pages/flow/form/begin-form/index.less b/web/src/pages/flow/form/begin-form/index.less deleted file mode 100644 index 0a03d4743..000000000 --- a/web/src/pages/flow/form/begin-form/index.less +++ /dev/null @@ -1,24 +0,0 @@ -.dynamicInputVariable { - background-color: #ebe9e950; - :global(.ant-collapse-content) { - background-color: #f6f6f657; - } - :global(.ant-collapse-content-box) { - padding: 0 !important; - } - margin-bottom: 20px; - .title { - font-weight: 600; - font-size: 16px; - } - - .addButton { - color: rgb(22, 119, 255); - font-weight: 600; - } -} - -.addButton { - color: rgb(22, 119, 255); - font-weight: 600; -} diff --git a/web/src/pages/flow/form/begin-form/index.tsx b/web/src/pages/flow/form/begin-form/index.tsx deleted file mode 100644 index 8df1181f1..000000000 --- a/web/src/pages/flow/form/begin-form/index.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { Button, Form, Input } from 'antd'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BeginQuery, IOperatorForm } from '../../interface'; -import { useEditQueryRecord } from './hooks'; -import { ModalForm } from './paramater-modal'; -import QueryTable from './query-table'; - -import styles from './index.less'; - -type FieldType = { - prologue?: string; -}; - -const BeginForm = ({ onValuesChange, form }: IOperatorForm) => { - const { t } = useTranslation(); - const { - ok, - currentRecord, - visible, - hideModal, - showModal, - otherThanCurrentQuery, - } = useEditQueryRecord({ - form, - onValuesChange, - }); - - const handleDeleteRecord = useCallback( - (idx: number) => { - const query = form?.getFieldValue('query') || []; - const nextQuery = query.filter( - (item: BeginQuery, index: number) => index !== idx, - ); - onValuesChange?.( - { query: nextQuery }, - { query: nextQuery, prologue: form?.getFieldValue('prologue') }, - ); - }, - [form, onValuesChange], - ); - - return ( - <Form.Provider - onFormFinish={(name, { values }) => { - if (name === 'queryForm') { - ok(values as BeginQuery); - } - }} - > - <Form - name="basicForm" - onValuesChange={onValuesChange} - autoComplete="off" - form={form} - layout="vertical" - > - <Form.Item<FieldType> - name={'prologue'} - label={t('chat.setAnOpener')} - tooltip={t('chat.setAnOpenerTip')} - initialValue={t('chat.setAnOpenerInitial')} - > - <Input.TextArea autoSize={{ minRows: 5 }} /> - </Form.Item> - {/* Create a hidden field to make Form instance record this */} - <Form.Item name="query" noStyle /> - - <Form.Item - shouldUpdate={(prevValues, curValues) => - prevValues.query !== curValues.query - } - > - {({ getFieldValue }) => { - const query: BeginQuery[] = getFieldValue('query') || []; - return ( - <QueryTable - data={query} - showModal={showModal} - deleteRecord={handleDeleteRecord} - ></QueryTable> - ); - }} - </Form.Item> - - <Button - htmlType="button" - style={{ margin: '0 8px' }} - onClick={() => showModal()} - icon={<PlusOutlined />} - block - className={styles.addButton} - > - {t('flow.addItem')} - </Button> - {visible && ( - <ModalForm - visible={visible} - hideModal={hideModal} - initialValue={currentRecord} - onOk={ok} - otherThanCurrentQuery={otherThanCurrentQuery} - /> - )} - </Form> - </Form.Provider> - ); -}; - -export default BeginForm; diff --git a/web/src/pages/flow/form/begin-form/paramater-modal.tsx b/web/src/pages/flow/form/begin-form/paramater-modal.tsx deleted file mode 100644 index 7d689601b..000000000 --- a/web/src/pages/flow/form/begin-form/paramater-modal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useResetFormOnCloseModal } from '@/hooks/logic-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { Form, Input, Modal, Select, Switch } from 'antd'; -import { DefaultOptionType } from 'antd/es/select'; -import { useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; -import { BeginQuery } from '../../interface'; -import BeginDynamicOptions from './begin-dynamic-options'; - -export const ModalForm = ({ - visible, - initialValue, - hideModal, - otherThanCurrentQuery, -}: IModalProps<BeginQuery> & { - initialValue: BeginQuery; - otherThanCurrentQuery: BeginQuery[]; -}) => { - const { t } = useTranslation(); - const [form] = Form.useForm(); - const options = useMemo(() => { - return Object.values(BeginQueryType).reduce<DefaultOptionType[]>( - (pre, cur) => { - const Icon = BeginQueryTypeIconMap[cur]; - - return [ - ...pre, - { - label: ( - <div className="flex items-center gap-2"> - <Icon - className={`size-${cur === BeginQueryType.Options ? 4 : 5}`} - ></Icon> - {cur} - </div> - ), - value: cur, - }, - ]; - }, - [], - ); - }, []); - - useResetFormOnCloseModal({ - form, - visible: visible, - }); - - useEffect(() => { - form.setFieldsValue(initialValue); - }, [form, initialValue]); - - const onOk = () => { - form.submit(); - }; - - return ( - <Modal - title={t('flow.variableSettings')} - open={visible} - onOk={onOk} - onCancel={hideModal} - centered - > - <Form form={form} layout="vertical" name="queryForm" autoComplete="false"> - <Form.Item - name="type" - label="Type" - rules={[{ required: true }]} - initialValue={BeginQueryType.Line} - > - <Select options={options} /> - </Form.Item> - <Form.Item - name="key" - label="Key" - rules={[ - { required: true }, - () => ({ - validator(_, value) { - if ( - !value || - !otherThanCurrentQuery.some((x) => x.key === value) - ) { - return Promise.resolve(); - } - return Promise.reject(new Error('The key cannot be repeated!')); - }, - }), - ]} - > - <Input /> - </Form.Item> - <Form.Item name="name" label="Name" rules={[{ required: true }]}> - <Input /> - </Form.Item> - <Form.Item - name="optional" - label={'Optional'} - valuePropName="checked" - initialValue={false} - > - <Switch /> - </Form.Item> - <Form.Item - shouldUpdate={(prevValues, curValues) => - prevValues.type !== curValues.type - } - > - {({ getFieldValue }) => { - const type: BeginQueryType = getFieldValue('type'); - return ( - type === BeginQueryType.Options && ( - <BeginDynamicOptions></BeginDynamicOptions> - ) - ); - }} - </Form.Item> - </Form> - </Modal> - ); -}; diff --git a/web/src/pages/flow/form/begin-form/query-table.tsx b/web/src/pages/flow/form/begin-form/query-table.tsx deleted file mode 100644 index c7614e682..000000000 --- a/web/src/pages/flow/form/begin-form/query-table.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; -import type { TableProps } from 'antd'; -import { Collapse, Space, Table, Tooltip } from 'antd'; -import { BeginQuery } from '../../interface'; - -import { useTranslation } from 'react-i18next'; -import styles from './index.less'; - -interface IProps { - data: BeginQuery[]; - deleteRecord(index: number): void; - showModal(index: number, record: BeginQuery): void; -} - -const QueryTable = ({ data, deleteRecord, showModal }: IProps) => { - const { t } = useTranslation(); - - const columns: TableProps<BeginQuery>['columns'] = [ - { - title: 'Key', - dataIndex: 'key', - key: 'key', - ellipsis: { - showTitle: false, - }, - render: (key) => ( - <Tooltip placement="topLeft" title={key}> - {key} - </Tooltip> - ), - }, - { - title: t('flow.name'), - dataIndex: 'name', - key: 'name', - ellipsis: { - showTitle: false, - }, - render: (name) => ( - <Tooltip placement="topLeft" title={name}> - {name} - </Tooltip> - ), - }, - { - title: t('flow.type'), - dataIndex: 'type', - key: 'type', - }, - { - title: t('flow.optional'), - dataIndex: 'optional', - key: 'optional', - render: (optional) => (optional ? 'Yes' : 'No'), - }, - { - title: t('common.action'), - key: 'action', - render: (_, record, idx) => ( - <Space> - <EditOutlined onClick={() => showModal(idx, record)} /> - <DeleteOutlined - className="cursor-pointer" - onClick={() => deleteRecord(idx)} - /> - </Space> - ), - }, - ]; - - return ( - <Collapse - defaultActiveKey={['1']} - className={styles.dynamicInputVariable} - items={[ - { - key: '1', - label: <span className={styles.title}>{t('flow.input')}</span>, - children: ( - <Table<BeginQuery> - columns={columns} - dataSource={data} - pagination={false} - /> - ), - }, - ]} - /> - ); -}; - -export default QueryTable; diff --git a/web/src/pages/flow/form/bing-form/index.tsx b/web/src/pages/flow/form/bing-form/index.tsx deleted file mode 100644 index b640f08d0..000000000 --- a/web/src/pages/flow/form/bing-form/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { useMemo } from 'react'; -import { BingCountryOptions, BingLanguageOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const BingForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const options = useMemo(() => { - return ['Webpages', 'News'].map((x) => ({ label: x, value: x })); - }, []); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10}></TopNItem> - <Form.Item label={t('channel')} name={'channel'}> - <Select options={options}></Select> - </Form.Item> - <Form.Item label={t('apiKey')} name={'api_key'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('country')} name={'country'}> - <Select options={BingCountryOptions}></Select> - </Form.Item> - <Form.Item label={t('language')} name={'language'}> - <Select options={BingLanguageOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default BingForm; diff --git a/web/src/pages/flow/form/categorize-form/dynamic-categorize.tsx b/web/src/pages/flow/form/categorize-form/dynamic-categorize.tsx deleted file mode 100644 index 7d3987971..000000000 --- a/web/src/pages/flow/form/categorize-form/dynamic-categorize.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { CloseOutlined, PlusOutlined } from '@ant-design/icons'; -import { useUpdateNodeInternals } from '@xyflow/react'; -import { - Button, - Collapse, - Flex, - Form, - FormListFieldData, - Input, - Select, -} from 'antd'; -import { FormInstance } from 'antd/lib'; -import { humanId } from 'human-id'; -import trim from 'lodash/trim'; -import { - ChangeEventHandler, - FocusEventHandler, - useCallback, - useEffect, - useState, -} from 'react'; -import { Operator } from '../../constant'; -import { useBuildFormSelectOptions } from '../../form-hooks'; - -import styles from './index.less'; - -interface IProps { - nodeId?: string; -} - -interface INameInputProps { - value?: string; - onChange?: (value: string) => void; - otherNames?: string[]; - validate(errors: string[]): void; -} - -const getOtherFieldValues = ( - form: FormInstance, - formListName: string = 'items', - field: FormListFieldData, - latestField: string, -) => - (form.getFieldValue([formListName]) ?? []) - .map((x: any) => x[latestField]) - .filter( - (x: string) => - x !== form.getFieldValue([formListName, field.name, latestField]), - ); - -const NameInput = ({ - value, - onChange, - otherNames, - validate, -}: INameInputProps) => { - const [name, setName] = useState<string | undefined>(); - const { t } = useTranslate('flow'); - - const handleNameChange: ChangeEventHandler<HTMLInputElement> = useCallback( - (e) => { - const val = e.target.value; - // trigger validation - if (otherNames?.some((x) => x === val)) { - validate([t('nameRepeatedMsg')]); - } else if (trim(val) === '') { - validate([t('nameRequiredMsg')]); - } else { - validate([]); - } - setName(val); - }, - [otherNames, validate, t], - ); - - const handleNameBlur: FocusEventHandler<HTMLInputElement> = useCallback( - (e) => { - const val = e.target.value; - if (otherNames?.every((x) => x !== val) && trim(val) !== '') { - onChange?.(val); - } - }, - [onChange, otherNames], - ); - - useEffect(() => { - setName(value); - }, [value]); - - return ( - <Input - value={name} - onChange={handleNameChange} - onBlur={handleNameBlur} - ></Input> - ); -}; - -const FormSet = ({ nodeId, field }: IProps & { field: FormListFieldData }) => { - const form = Form.useFormInstance(); - const { t } = useTranslate('flow'); - const buildCategorizeToOptions = useBuildFormSelectOptions( - Operator.Categorize, - nodeId, - ); - - return ( - <section> - <Form.Item - label={t('categoryName')} - name={[field.name, 'name']} - validateTrigger={['onChange', 'onBlur']} - rules={[ - { - required: true, - whitespace: true, - message: t('nameMessage'), - }, - ]} - > - <NameInput - otherNames={getOtherFieldValues(form, 'items', field, 'name')} - validate={(errors: string[]) => - form.setFields([ - { - name: ['items', field.name, 'name'], - errors, - }, - ]) - } - ></NameInput> - </Form.Item> - <Form.Item label={t('description')} name={[field.name, 'description']}> - <Input.TextArea rows={3} /> - </Form.Item> - <Form.Item label={t('examples')} name={[field.name, 'examples']}> - <Input.TextArea rows={3} /> - </Form.Item> - <Form.Item label={t('nextStep')} name={[field.name, 'to']}> - <Select - allowClear - options={buildCategorizeToOptions( - getOtherFieldValues(form, 'items', field, 'to'), - )} - /> - </Form.Item> - <Form.Item hidden name={[field.name, 'index']}> - <Input /> - </Form.Item> - </section> - ); -}; - -const DynamicCategorize = ({ nodeId }: IProps) => { - const updateNodeInternals = useUpdateNodeInternals(); - const form = Form.useFormInstance(); - - const { t } = useTranslate('flow'); - - return ( - <> - <Form.List name="items"> - {(fields, { add, remove }) => { - const handleAdd = () => { - const idx = form.getFieldValue([ - 'items', - fields.at(-1)?.name, - 'index', - ]); - add({ - name: humanId(), - index: fields.length === 0 ? 0 : idx + 1, - }); - if (nodeId) updateNodeInternals(nodeId); - }; - - return ( - <Flex gap={18} vertical> - {fields.map((field) => ( - <Collapse - size="small" - key={field.key} - className={styles.caseCard} - items={[ - { - key: field.key, - label: ( - <div className="flex justify-between"> - <span> - {form.getFieldValue(['items', field.name, 'name'])} - </span> - <CloseOutlined - onClick={() => { - remove(field.name); - }} - /> - </div> - ), - children: ( - <FormSet nodeId={nodeId} field={field}></FormSet> - ), - }, - ]} - ></Collapse> - ))} - - <Button - type="dashed" - onClick={handleAdd} - block - className={styles.addButton} - icon={<PlusOutlined />} - > - {t('addCategory')} - </Button> - </Flex> - ); - }} - </Form.List> - </> - ); -}; - -export default DynamicCategorize; diff --git a/web/src/pages/flow/form/categorize-form/hooks.ts b/web/src/pages/flow/form/categorize-form/hooks.ts deleted file mode 100644 index a7e8f23f3..000000000 --- a/web/src/pages/flow/form/categorize-form/hooks.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - ICategorizeItem, - ICategorizeItemResult, -} from '@/interfaces/database/flow'; -import omit from 'lodash/omit'; -import { useCallback } from 'react'; -import { IOperatorForm } from '../../interface'; - -/** - * Convert the list in the following form into an object - * { - "items": [ - { - "name": "Categorize 1", - "description": "111", - "examples": "ddd", - "to": "Retrieval:LazyEelsStick" - } - ] - } -*/ -const buildCategorizeObjectFromList = (list: Array<ICategorizeItem>) => { - return list.reduce<ICategorizeItemResult>((pre, cur) => { - if (cur?.name) { - pre[cur.name] = omit(cur, 'name'); - } - return pre; - }, {}); -}; - -export const useHandleFormValuesChange = ({ - onValuesChange, -}: IOperatorForm) => { - const handleValuesChange = useCallback( - (changedValues: any, values: any) => { - onValuesChange?.(changedValues, { - ...omit(values, 'items'), - category_description: buildCategorizeObjectFromList(values.items), - }); - }, - [onValuesChange], - ); - - return { handleValuesChange }; -}; diff --git a/web/src/pages/flow/form/categorize-form/index.less b/web/src/pages/flow/form/categorize-form/index.less deleted file mode 100644 index 6d78e80f9..000000000 --- a/web/src/pages/flow/form/categorize-form/index.less +++ /dev/null @@ -1,13 +0,0 @@ -@lightBackgroundColor: rgba(150, 150, 150, 0.07); -@darkBackgroundColor: rgba(150, 150, 150, 0.12); - -.caseCard { - :global(.ant-collapse-content) { - background-color: @darkBackgroundColor; - } -} - -.addButton { - color: rgb(22, 119, 255); - font-weight: 600; -} diff --git a/web/src/pages/flow/form/categorize-form/index.tsx b/web/src/pages/flow/form/categorize-form/index.tsx deleted file mode 100644 index cb6651a24..000000000 --- a/web/src/pages/flow/form/categorize-form/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; -import DynamicCategorize from './dynamic-categorize'; -import { useHandleFormValuesChange } from './hooks'; - -const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const { handleValuesChange } = useHandleFormValuesChange({ - form, - nodeId: node?.id, - onValuesChange, - }); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={handleValuesChange} - initialValues={{ items: [{}] }} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect></LLMSelect> - </Form.Item> - <MessageHistoryWindowSizeItem - initialValue={1} - ></MessageHistoryWindowSizeItem> - <DynamicCategorize nodeId={node?.id}></DynamicCategorize> - </Form> - ); -}; - -export default CategorizeForm; diff --git a/web/src/pages/flow/form/code-form/dynamic-input-variable.tsx b/web/src/pages/flow/form/code-form/dynamic-input-variable.tsx deleted file mode 100644 index 8fc55c0be..000000000 --- a/web/src/pages/flow/form/code-form/dynamic-input-variable.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Form, Input, Select } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; -import { FormCollapse } from '../components/dynamic-input-variable'; - -type DynamicInputVariableProps = { - name?: string; - node?: RAGFlowNodeType; -}; - -export const DynamicInputVariable = ({ - name = 'arguments', - node, -}: DynamicInputVariableProps) => { - const { t } = useTranslation(); - - const valueOptions = useBuildComponentIdSelectOptions( - node?.id, - node?.parentId, - ); - - return ( - <FormCollapse title={t('flow.inputVariables')}> - <Form.List name={name}> - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - <div key={key} className="flex items-center gap-2 pb-4"> - <Form.Item - {...restField} - name={[name, 'name']} - className="m-0 flex-1" - > - <Input /> - </Form.Item> - <Form.Item - {...restField} - name={[name, 'component_id']} - className="m-0 flex-1" - > - <Select - placeholder={t('common.pleaseSelect')} - options={valueOptions} - ></Select> - </Form.Item> - <MinusCircleOutlined onClick={() => remove(name)} /> - </div> - ))} - <Form.Item> - <Button - type="dashed" - onClick={() => add()} - block - icon={<PlusOutlined />} - > - {t('flow.addVariable')} - </Button> - </Form.Item> - </> - )} - </Form.List> - </FormCollapse> - ); -}; diff --git a/web/src/pages/flow/form/code-form/dynamic-output-variable.tsx b/web/src/pages/flow/form/code-form/dynamic-output-variable.tsx deleted file mode 100644 index 0e7e3a03c..000000000 --- a/web/src/pages/flow/form/code-form/dynamic-output-variable.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Form, Input, Select } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { FormCollapse } from '../components/dynamic-input-variable'; - -type DynamicOutputVariableProps = { - name?: string; -}; - -const options = [ - 'String', - 'Number', - 'Boolean', - 'Array[String]', - 'Array[Number]', - 'Object', -].map((x) => ({ label: x, value: x })); - -export const DynamicOutputVariable = ({ - name = 'output', -}: DynamicOutputVariableProps) => { - const { t } = useTranslation(); - - return ( - <FormCollapse title={t('flow.output')}> - <Form.List name={name}> - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - <div key={key} className="flex items-center gap-2 pb-4"> - <Form.Item - {...restField} - name={[name, 'first']} - className="m-0 flex-1" - > - <Input /> - </Form.Item> - <Form.Item - {...restField} - name={[name, 'last']} - className="m-0 flex-1" - > - <Select - placeholder={t('common.pleaseSelect')} - options={options} - ></Select> - </Form.Item> - <MinusCircleOutlined onClick={() => remove(name)} /> - </div> - ))} - <Form.Item> - <Button - type="dashed" - onClick={() => add()} - block - icon={<PlusOutlined />} - > - {t('flow.addVariable')} - </Button> - </Form.Item> - </> - )} - </Form.List> - </FormCollapse> - ); -}; diff --git a/web/src/pages/flow/form/code-form/index.less b/web/src/pages/flow/form/code-form/index.less deleted file mode 100644 index cde868bb6..000000000 --- a/web/src/pages/flow/form/code-form/index.less +++ /dev/null @@ -1,16 +0,0 @@ -.languageItem { - margin: 0; - :global(.ant-select-selector) { - background: transparent !important; - border: none !important; - box-shadow: none !important; - } - :global(.ant-select-selector:hover) { - border: none !important; - box-shadow: none !important; - } - :global(.ant-select-focused .ant-select-selector) { - border: none !important; - box-shadow: none !important; - } -} diff --git a/web/src/pages/flow/form/code-form/index.tsx b/web/src/pages/flow/form/code-form/index.tsx deleted file mode 100644 index 754ffa3f4..000000000 --- a/web/src/pages/flow/form/code-form/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import Editor, { loader } from '@monaco-editor/react'; -import { Form, Select } from 'antd'; -import { IOperatorForm } from '../../interface'; -import { DynamicInputVariable } from './dynamic-input-variable'; - -import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; -import { ICodeForm } from '@/interfaces/database/flow'; -import { useCallback } from 'react'; -import useGraphStore from '../../store'; -import styles from './index.less'; - -loader.config({ paths: { vs: '/vs' } }); - -const options = [ - ProgrammingLanguage.Python, - ProgrammingLanguage.Javascript, -].map((x) => ({ value: x, label: x })); - -const CodeForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const formData = node?.data.form as ICodeForm; - const updateNodeForm = useGraphStore((state) => state.updateNodeForm); - - const handleChange = useCallback( - (value: ProgrammingLanguage) => { - if (node?.id) { - updateNodeForm( - node?.id, - CodeTemplateStrMap[value as ProgrammingLanguage], - ['script'], - ); - } - }, - [node?.id, updateNodeForm], - ); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item - name={'script'} - label={ - <Form.Item name={'lang'} className={styles.languageItem}> - <Select - defaultValue={ProgrammingLanguage.Python} - popupMatchSelectWidth={false} - options={options} - onChange={handleChange} - /> - </Form.Item> - } - className="bg-gray-100 rounded dark:bg-gray-800" - > - <Editor - height={600} - theme="vs-dark" - language={formData.lang} - options={{ - minimap: { enabled: false }, - automaticLayout: true, - }} - /> - </Form.Item> - </Form> - ); -}; - -export default CodeForm; diff --git a/web/src/pages/flow/form/components/dynamic-input-variable.tsx b/web/src/pages/flow/form/components/dynamic-input-variable.tsx deleted file mode 100644 index 269719dcd..000000000 --- a/web/src/pages/flow/form/components/dynamic-input-variable.tsx +++ /dev/null @@ -1,133 +0,0 @@ -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 { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; - -import styles from './index.less'; - -interface IProps { - name?: string; - node?: RAGFlowNodeType; - title?: string; -} - -enum VariableType { - Reference = 'reference', - Input = 'input', -} - -const getVariableName = (type: string) => - type === VariableType.Reference ? 'component_id' : 'value'; - -const DynamicVariableForm = ({ name: formName, node }: IProps) => { - const nextFormName = formName || 'query'; - const { t } = useTranslation(); - const valueOptions = useBuildComponentIdSelectOptions( - 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([nextFormName, name, 'component_id'], undefined); - form.setFieldValue([nextFormName, name, 'value'], undefined); - }, 0); - }, - [form, nextFormName], - ); - - return ( - <Form.List name={nextFormName}> - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - <Flex key={key} gap={10} align={'baseline'}> - <Form.Item - {...restField} - name={[name, 'type']} - className={styles.variableType} - > - <Select - options={options} - onChange={handleTypeChange(name)} - ></Select> - </Form.Item> - <Form.Item noStyle dependencies={[name, 'type']}> - {({ getFieldValue }) => { - const type = getFieldValue([nextFormName, name, 'type']); - return ( - <Form.Item - {...restField} - name={[name, getVariableName(type)]} - className={styles.variableValue} - > - {type === VariableType.Reference ? ( - <Select - placeholder={t('common.pleaseSelect')} - options={valueOptions} - ></Select> - ) : ( - <Input placeholder={t('common.pleaseInput')} /> - )} - </Form.Item> - ); - }} - </Form.Item> - <MinusCircleOutlined onClick={() => remove(name)} /> - </Flex> - ))} - <Form.Item> - <Button - type="dashed" - onClick={() => add({ type: VariableType.Reference })} - block - icon={<PlusOutlined />} - className={styles.addButton} - > - {t('flow.addVariable')} - </Button> - </Form.Item> - </> - )} - </Form.List> - ); -}; - -export function FormCollapse({ - children, - title, -}: PropsWithChildren<{ title: string }>) { - return ( - <Collapse - className={styles.dynamicInputVariable} - defaultActiveKey={['1']} - items={[ - { - key: '1', - label: <span className={styles.title}>{title}</span>, - children, - }, - ]} - /> - ); -} - -const DynamicInputVariable = ({ name, node, title }: IProps) => { - const { t } = useTranslation(); - return ( - <FormCollapse title={title || t('flow.input')}> - <DynamicVariableForm name={name} node={node}></DynamicVariableForm> - </FormCollapse> - ); -}; - -export default DynamicInputVariable; diff --git a/web/src/pages/flow/form/components/index.less b/web/src/pages/flow/form/components/index.less deleted file mode 100644 index 344514d9e..000000000 --- a/web/src/pages/flow/form/components/index.less +++ /dev/null @@ -1,22 +0,0 @@ -.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/flow/form/concentrator-form/index.tsx b/web/src/pages/flow/form/concentrator-form/index.tsx deleted file mode 100644 index d409f3099..000000000 --- a/web/src/pages/flow/form/concentrator-form/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Form } from 'antd'; -import { IOperatorForm } from '../../interface'; - -const ConcentratorForm = ({ onValuesChange, form }: IOperatorForm) => { - return ( - <Form - name="basic" - labelCol={{ span: 8 }} - wrapperCol={{ span: 16 }} - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - ></Form> - ); -}; - -export default ConcentratorForm; diff --git a/web/src/pages/flow/form/crawler-form/index.tsx b/web/src/pages/flow/form/crawler-form/index.tsx deleted file mode 100644 index 8ef5f14d6..000000000 --- a/web/src/pages/flow/form/crawler-form/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { useMemo } from 'react'; -import { CrawlerResultOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; -const CrawlerForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const crawlerResultOptions = useMemo(() => { - return CrawlerResultOptions.map((x) => ({ - value: x, - label: t(`crawlerResultOptions.${x}`), - })); - }, [t]); - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item label={t('proxy')} name={'proxy'}> - <Input placeholder="like: http://127.0.0.1:8888"></Input> - </Form.Item> - <Form.Item - label={t('extractType')} - name={'extract_type'} - initialValue="markdown" - > - <Select options={crawlerResultOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default CrawlerForm; diff --git a/web/src/pages/flow/form/deepl-form/index.tsx b/web/src/pages/flow/form/deepl-form/index.tsx deleted file mode 100644 index 1fc8cfc26..000000000 --- a/web/src/pages/flow/form/deepl-form/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { DeepLSourceLangOptions, DeepLTargetLangOptions } from '../../constant'; -import { useBuildSortOptions } from '../../form-hooks'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const options = useBuildSortOptions(); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={5}></TopNItem> - <Form.Item label={t('authKey')} name={'auth_key'}> - <Select options={options}></Select> - </Form.Item> - <Form.Item label={t('sourceLang')} name={'source_lang'}> - <Select options={DeepLSourceLangOptions}></Select> - </Form.Item> - <Form.Item label={t('targetLang')} name={'target_lang'}> - <Select options={DeepLTargetLangOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default DeepLForm; diff --git a/web/src/pages/flow/form/duckduckgo-form/index.tsx b/web/src/pages/flow/form/duckduckgo-form/index.tsx deleted file mode 100644 index 53462da31..000000000 --- a/web/src/pages/flow/form/duckduckgo-form/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { useMemo } from 'react'; -import { Channel } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const DuckDuckGoForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const options = useMemo(() => { - return Object.values(Channel).map((x) => ({ value: x, label: t(x) })); - }, [t]); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10}></TopNItem> - <Form.Item - label={t('channel')} - name={'channel'} - tooltip={t('channelTip')} - initialValue={'text'} - > - <Select options={options}></Select> - </Form.Item> - </Form> - ); -}; - -export default DuckDuckGoForm; diff --git a/web/src/pages/flow/form/email-form/index.tsx b/web/src/pages/flow/form/email-form/index.tsx deleted file mode 100644 index 9bee7997f..000000000 --- a/web/src/pages/flow/form/email-form/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - - {/* SMTP服务器配置 */} - <Form.Item label={t('smtpServer')} name={'smtp_server'}> - <Input placeholder="smtp.example.com" /> - </Form.Item> - <Form.Item label={t('smtpPort')} name={'smtp_port'}> - <Input type="number" placeholder="587" /> - </Form.Item> - <Form.Item label={t('senderEmail')} name={'email'}> - <Input placeholder="sender@example.com" /> - </Form.Item> - <Form.Item label={t('authCode')} name={'password'}> - <Input.Password placeholder="your_password" /> - </Form.Item> - <Form.Item label={t('senderName')} name={'sender_name'}> - <Input placeholder="Sender Name" /> - </Form.Item> - - {/* 动态参数说明 */} - <div style={{ marginBottom: 24 }}> - <h4>{t('dynamicParameters')}</h4> - <div>{t('jsonFormatTip')}</div> - <pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}> - {`{ - "to_email": "recipient@example.com", - "cc_email": "cc@example.com", - "subject": "Email Subject", - "content": "Email Content" -}`} - </pre> - </div> - </Form> - ); -}; - -export default EmailForm; diff --git a/web/src/pages/flow/form/exesql-form/index.tsx b/web/src/pages/flow/form/exesql-form/index.tsx deleted file mode 100644 index f88f593d0..000000000 --- a/web/src/pages/flow/form/exesql-form/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { useTestDbConnect } from '@/hooks/flow-hooks'; -import { Button, Flex, Form, Input, InputNumber, Select } from 'antd'; -import { useCallback } from 'react'; -import { ExeSQLOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const { testDbConnect, loading } = useTestDbConnect(); - - const handleTest = useCallback(async () => { - const ret = await form?.validateFields(); - testDbConnect(ret); - }, [form, testDbConnect]); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect></LLMSelect> - </Form.Item> - <Form.Item - label={t('dbType')} - name={'db_type'} - rules={[{ required: true }]} - > - <Select options={ExeSQLOptions}></Select> - </Form.Item> - <Form.Item - label={t('database')} - name={'database'} - rules={[{ required: true }]} - > - <Input></Input> - </Form.Item> - <Form.Item - label={t('username')} - name={'username'} - rules={[{ required: true }]} - > - <Input></Input> - </Form.Item> - <Form.Item label={t('host')} name={'host'} rules={[{ required: true }]}> - <Input></Input> - </Form.Item> - <Form.Item label={t('port')} name={'port'} rules={[{ required: true }]}> - <InputNumber></InputNumber> - </Form.Item> - <Form.Item - label={t('password')} - name={'password'} - rules={[{ required: true }]} - > - <Input.Password></Input.Password> - </Form.Item> - <Form.Item - label={t('loop')} - name={'loop'} - tooltip={t('loopTip')} - rules={[{ required: true }]} - > - <InputNumber></InputNumber> - </Form.Item> - <TopNItem initialValue={30} max={1000}></TopNItem> - <Flex justify={'end'}> - <Button type={'primary'} loading={loading} onClick={handleTest}> - Test - </Button> - </Flex> - </Form> - ); -}; - -export default ExeSQLForm; diff --git a/web/src/pages/flow/form/generate-form/dynamic-parameters.tsx b/web/src/pages/flow/form/generate-form/dynamic-parameters.tsx deleted file mode 100644 index 463437da3..000000000 --- a/web/src/pages/flow/form/generate-form/dynamic-parameters.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { EditableCell, EditableRow } from '@/components/editable-cell'; -import { useTranslate } from '@/hooks/common-hooks'; -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { DeleteOutlined } from '@ant-design/icons'; -import { Button, Flex, Select, Table, TableProps } from 'antd'; -import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; -import { IGenerateParameter } from '../../interface'; -import { useHandleOperateParameters } from './hooks'; - -import styles from './index.less'; -interface IProps { - node?: RAGFlowNodeType; -} - -const components = { - body: { - row: EditableRow, - cell: EditableCell, - }, -}; - -const DynamicParameters = ({ node }: IProps) => { - const nodeId = node?.id; - const { t } = useTranslate('flow'); - - const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); - const { - dataSource, - handleAdd, - handleRemove, - handleSave, - handleComponentIdChange, - } = useHandleOperateParameters(nodeId!); - - const columns: TableProps<IGenerateParameter>['columns'] = [ - { - title: t('key'), - dataIndex: 'key', - key: 'key', - width: '40%', - onCell: (record: IGenerateParameter) => ({ - record, - editable: true, - dataIndex: 'key', - title: 'key', - handleSave, - }), - }, - { - title: t('value'), - dataIndex: 'component_id', - key: 'component_id', - align: 'center', - width: '40%', - render(text, record) { - return ( - <Select - style={{ width: '100%' }} - allowClear - options={options} - value={text} - onChange={handleComponentIdChange(record)} - /> - ); - }, - }, - { - title: t('operation'), - dataIndex: 'operation', - width: 20, - key: 'operation', - align: 'center', - fixed: 'right', - render(_, record) { - return <DeleteOutlined onClick={handleRemove(record.id)} />; - }, - }, - ]; - - return ( - <section> - <Flex justify="end"> - <Button size="small" onClick={handleAdd}> - {t('add')} - </Button> - </Flex> - <Table - dataSource={dataSource} - columns={columns} - rowKey={'id'} - className={styles.variableTable} - components={components} - rowClassName={() => styles.editableRow} - scroll={{ x: true }} - bordered - /> - </section> - ); -}; - -export default DynamicParameters; diff --git a/web/src/pages/flow/form/generate-form/hooks.ts b/web/src/pages/flow/form/generate-form/hooks.ts deleted file mode 100644 index 8aa179cf9..000000000 --- a/web/src/pages/flow/form/generate-form/hooks.ts +++ /dev/null @@ -1,70 +0,0 @@ -import get from 'lodash/get'; -import { useCallback, useMemo } from 'react'; -import { v4 as uuid } from 'uuid'; -import { IGenerateParameter } 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.parameters', []) as IGenerateParameter[], - [node], - ); - - const handleComponentIdChange = useCallback( - (row: IGenerateParameter) => (value: string) => { - const newData = [...dataSource]; - const index = newData.findIndex((item) => row.id === item.id); - const item = newData[index]; - newData.splice(index, 1, { - ...item, - component_id: value, - }); - - updateNodeForm(nodeId, { parameters: newData }); - }, - [updateNodeForm, nodeId, dataSource], - ); - - const handleRemove = useCallback( - (id?: string) => () => { - const newData = dataSource.filter((item) => item.id !== id); - updateNodeForm(nodeId, { parameters: newData }); - }, - [updateNodeForm, nodeId, dataSource], - ); - - const handleAdd = useCallback(() => { - updateNodeForm(nodeId, { - parameters: [ - ...dataSource, - { - id: uuid(), - key: '', - component_id: undefined, - }, - ], - }); - }, [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, { parameters: newData }); - }; - - return { - handleAdd, - handleRemove, - handleComponentIdChange, - handleSave, - dataSource, - }; -}; diff --git a/web/src/pages/flow/form/generate-form/index.less b/web/src/pages/flow/form/generate-form/index.less deleted file mode 100644 index 8de5a4f53..000000000 --- a/web/src/pages/flow/form/generate-form/index.less +++ /dev/null @@ -1,21 +0,0 @@ -.variableTable { - margin-top: 14px; -} -.editableRow { - :global(.editable-cell) { - position: relative; - } - - :global(.editable-cell-value-wrap) { - padding: 5px 12px; - cursor: pointer; - height: 30px !important; - } - &:hover { - :global(.editable-cell-value-wrap) { - padding: 4px 11px; - border: 1px solid #d9d9d9; - border-radius: 2px; - } - } -} diff --git a/web/src/pages/flow/form/generate-form/index.tsx b/web/src/pages/flow/form/generate-form/index.tsx deleted file mode 100644 index e2e7ed5b7..000000000 --- a/web/src/pages/flow/form/generate-form/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item'; -import { PromptEditor } from '@/components/prompt-editor'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Switch } from 'antd'; -import { IOperatorForm } from '../../interface'; -import LLMToolsSelect from '@/components/llm-tools-select'; -import { useState } from 'react'; - -const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const [isCurrentLlmSupportTools, setCurrentLlmSupportTools] = useState(false); - - const onLlmSelectChanged = (_: string, option: any) => { - setCurrentLlmSupportTools(option.is_tools); - }; - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect onInitialValue={onLlmSelectChanged} onChange={onLlmSelectChanged}></LLMSelect> - </Form.Item> - <Form.Item - name={['prompt']} - label={t('systemPrompt')} - initialValue={t('promptText')} - tooltip={t('promptTip')} - rules={[ - { - required: true, - message: t('promptMessage'), - }, - ]} - > - {/* <Input.TextArea rows={8}></Input.TextArea> */} - <PromptEditor></PromptEditor> - </Form.Item> - <Form.Item - name={'llm_enabled_tools'} - label={t('modelEnabledTools', { keyPrefix: 'chat' })} - tooltip={t('modelEnabledToolsTip', { keyPrefix: 'chat' })} - > - <LLMToolsSelect disabled={!isCurrentLlmSupportTools}></LLMToolsSelect> - </Form.Item> - <Form.Item - name={['cite']} - label={t('cite')} - initialValue={true} - valuePropName="checked" - tooltip={t('citeTip')} - > - <Switch /> - </Form.Item> - <MessageHistoryWindowSizeItem - initialValue={12} - ></MessageHistoryWindowSizeItem> - </Form> - ); -}; - -export default GenerateForm; diff --git a/web/src/pages/flow/form/github-form/index.tsx b/web/src/pages/flow/form/github-form/index.tsx deleted file mode 100644 index 691dd5d36..000000000 --- a/web/src/pages/flow/form/github-form/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { Form } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const GithubForm = ({ onValuesChange, form, node }: IOperatorForm) => { - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={5}></TopNItem> - </Form> - ); -}; - -export default GithubForm; diff --git a/web/src/pages/flow/form/google-form/index.tsx b/web/src/pages/flow/form/google-form/index.tsx deleted file mode 100644 index 75bd3fb5e..000000000 --- a/web/src/pages/flow/form/google-form/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { GoogleCountryOptions, GoogleLanguageOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const GoogleForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10}></TopNItem> - <Form.Item label={t('apiKey')} name={'api_key'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('country')} name={'country'}> - <Select options={GoogleCountryOptions}></Select> - </Form.Item> - <Form.Item label={t('language')} name={'language'}> - <Select options={GoogleLanguageOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default GoogleForm; diff --git a/web/src/pages/flow/form/google-scholar-form/index.tsx b/web/src/pages/flow/form/google-scholar-form/index.tsx deleted file mode 100644 index 4e87fac25..000000000 --- a/web/src/pages/flow/form/google-scholar-form/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { DatePicker, DatePickerProps, Form, Select, Switch } from 'antd'; -import dayjs from 'dayjs'; -import { useCallback, useMemo } from 'react'; -import { useBuildSortOptions } from '../../form-hooks'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -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 <DatePicker picker="year" onChange={handleChange} value={nextValue} />; -}; - -const GoogleScholarForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const options = useBuildSortOptions(); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={5}></TopNItem> - <Form.Item - label={t('sortBy')} - name={'sort_by'} - initialValue={'relevance'} - > - <Select options={options}></Select> - </Form.Item> - <Form.Item label={t('yearLow')} name={'year_low'}> - <YearPicker /> - </Form.Item> - <Form.Item label={t('yearHigh')} name={'year_high'}> - <YearPicker /> - </Form.Item> - <Form.Item - label={t('patents')} - name={'patents'} - valuePropName="checked" - initialValue={true} - > - <Switch></Switch> - </Form.Item> - </Form> - ); -}; - -export default GoogleScholarForm; diff --git a/web/src/pages/flow/form/invoke-form/dynamic-variables.tsx b/web/src/pages/flow/form/invoke-form/dynamic-variables.tsx deleted file mode 100644 index 9571c66c2..000000000 --- a/web/src/pages/flow/form/invoke-form/dynamic-variables.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { EditableCell, EditableRow } from '@/components/editable-cell'; -import { useTranslate } from '@/hooks/common-hooks'; -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { DeleteOutlined } from '@ant-design/icons'; -import { Button, Collapse, Flex, Input, Select, Table, TableProps } from 'antd'; -import { trim } from 'lodash'; -import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; -import { IInvokeVariable } from '../../interface'; -import { useHandleOperateParameters } from './hooks'; -import styles from './index.less'; - -interface IProps { - node?: RAGFlowNodeType; -} - -const components = { - body: { - row: EditableRow, - cell: EditableCell, - }, -}; - -const DynamicVariablesForm = ({ node }: IProps) => { - const nodeId = node?.id; - const { t } = useTranslate('flow'); - - const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); - const { - dataSource, - handleAdd, - handleRemove, - handleSave, - handleComponentIdChange, - handleValueChange, - } = useHandleOperateParameters(nodeId!); - - const columns: TableProps<IInvokeVariable>['columns'] = [ - { - title: t('key'), - dataIndex: 'key', - key: 'key', - onCell: (record: IInvokeVariable) => ({ - record, - editable: true, - dataIndex: 'key', - title: 'key', - handleSave, - }), - }, - { - title: t('componentId'), - dataIndex: 'component_id', - key: 'component_id', - align: 'center', - width: 140, - render(text, record) { - return ( - <Select - style={{ width: '100%' }} - allowClear - options={options} - value={text} - disabled={trim(record.value) !== ''} - onChange={handleComponentIdChange(record)} - /> - ); - }, - }, - { - title: t('value'), - dataIndex: 'value', - key: 'value', - align: 'center', - width: 140, - render(text, record) { - return ( - <Input - value={text} - disabled={!!record.component_id} - onChange={handleValueChange(record)} - /> - ); - }, - }, - { - title: t('operation'), - dataIndex: 'operation', - width: 20, - key: 'operation', - align: 'center', - fixed: 'right', - render(_, record) { - return <DeleteOutlined onClick={handleRemove(record.id)} />; - }, - }, - ]; - - return ( - <Collapse - className={styles.dynamicParameterVariable} - defaultActiveKey={['1']} - items={[ - { - key: '1', - label: ( - <Flex justify={'space-between'}> - <span className={styles.title}>{t('parameter')}</span> - <Button size="small" onClick={handleAdd}> - {t('add')} - </Button> - </Flex> - ), - children: ( - <Table - dataSource={dataSource} - columns={columns} - rowKey={'id'} - components={components} - rowClassName={() => styles.editableRow} - scroll={{ x: true }} - bordered - /> - ), - }, - ]} - /> - ); -}; - -export default DynamicVariablesForm; diff --git a/web/src/pages/flow/form/invoke-form/hooks.ts b/web/src/pages/flow/form/invoke-form/hooks.ts deleted file mode 100644 index 951cd42ae..000000000 --- a/web/src/pages/flow/form/invoke-form/hooks.ts +++ /dev/null @@ -1,97 +0,0 @@ -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<HTMLInputElement> => - (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/flow/form/invoke-form/index.less b/web/src/pages/flow/form/invoke-form/index.less deleted file mode 100644 index e5bada1ef..000000000 --- a/web/src/pages/flow/form/invoke-form/index.less +++ /dev/null @@ -1,44 +0,0 @@ -.editableRow { - :global(.editable-cell) { - position: relative; - } - - :global(.editable-cell-value-wrap) { - padding: 5px 12px; - cursor: pointer; - height: 30px !important; - } - &:hover { - :global(.editable-cell-value-wrap) { - padding: 4px 11px; - border: 1px solid #d9d9d9; - border-radius: 2px; - } - } -} - -.dynamicParameterVariable { - background-color: #ebe9e950; - :global(.ant-collapse-content) { - background-color: #f6f6f634; - } - :global(.ant-collapse-content-box) { - padding: 0 !important; - } - 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/flow/form/invoke-form/index.tsx b/web/src/pages/flow/form/invoke-form/index.tsx deleted file mode 100644 index a62723ebc..000000000 --- a/web/src/pages/flow/form/invoke-form/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import Editor, { loader } from '@monaco-editor/react'; -import { Form, Input, InputNumber, Select, Space, Switch } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { IOperatorForm } from '../../interface'; -import DynamicVariablesForm from './dynamic-variables'; - -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 ( - <Space> - <InputNumber value={value} onChange={onChange} /> {t('common.s')} - </Space> - ); -}; - -const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslation(); - - return ( - <> - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <Form.Item name={'url'} label={t('flow.url')}> - <Input /> - </Form.Item> - <Form.Item - name={'method'} - label={t('flow.method')} - initialValue={Method.GET} - > - <Select options={MethodOptions} /> - </Form.Item> - <Form.Item name={'timeout'} label={t('flow.timeout')}> - <TimeoutInput></TimeoutInput> - </Form.Item> - <Form.Item name={'headers'} label={t('flow.headers')}> - <Editor height={200} defaultLanguage="json" theme="vs-dark" /> - </Form.Item> - <Form.Item name={'proxy'} label={t('flow.proxy')}> - <Input /> - </Form.Item> - <Form.Item - name={'clean_html'} - label={t('flow.cleanHtml')} - tooltip={t('flow.cleanHtmlTip')} - > - <Switch /> - </Form.Item> - <Form.Item name={'datatype'} label={t('flow.datatype')}> - <Select - options={[ - { value: 'json', label: 'application/json' }, - { value: 'formdata', label: 'multipart/form-data' }, - ]} - allowClear={true} - ></Select> - </Form.Item> - <DynamicVariablesForm node={node}></DynamicVariablesForm> - </Form> - </> - ); -}; - -export default InvokeForm; diff --git a/web/src/pages/flow/form/iteration-from/index.tsx b/web/src/pages/flow/form/iteration-from/index.tsx deleted file mode 100644 index bff468ff6..000000000 --- a/web/src/pages/flow/form/iteration-from/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { CommaIcon, SemicolonIcon } from '@/assets/icon/next-icon'; -import { Form, Select } from 'antd'; -import { - CornerDownLeft, - IndentIncrease, - Minus, - Slash, - Underline, -} from 'lucide-react'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const optionList = [ - { - value: ',', - icon: CommaIcon, - text: 'comma', - }, - { - value: '\n', - icon: CornerDownLeft, - text: 'lineBreak', - }, - { - value: 'tab', - icon: IndentIncrease, - text: 'tab', - }, - { - value: '_', - icon: Underline, - text: 'underline', - }, - { - value: '/', - icon: Slash, - text: 'diagonal', - }, - { - value: '-', - icon: Minus, - text: 'minus', - }, - { - value: ';', - icon: SemicolonIcon, - text: 'semicolon', - }, -]; - -const IterationForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslation(); - - const options = useMemo(() => { - return optionList.map((x) => { - let Icon = x.icon; - - return { - value: x.value, - label: ( - <div className="flex items-center gap-2"> - <Icon className={'size-4'}></Icon> - {t(`flow.delimiterOptions.${x.text}`)} - </div> - ), - }; - }); - }, [t]); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item - name={['delimiter']} - label={t('knowledgeDetails.delimiter')} - initialValue={`\\n!?;。;!?`} - rules={[{ required: true }]} - tooltip={t('flow.delimiterTip')} - > - <Select options={options}></Select> - </Form.Item> - </Form> - ); -}; - -export default IterationForm; diff --git a/web/src/pages/flow/form/jin10-form/index.tsx b/web/src/pages/flow/form/jin10-form/index.tsx deleted file mode 100644 index aa9bb169f..000000000 --- a/web/src/pages/flow/form/jin10-form/index.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { useMemo } from 'react'; -import { - Jin10CalendarDatashapeOptions, - Jin10CalendarTypeOptions, - Jin10FlashTypeOptions, - Jin10SymbolsDatatypeOptions, - Jin10SymbolsTypeOptions, - Jin10TypeOptions, -} from '../../constant'; -import { IOperatorForm } from '../../interface'; -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 ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item label={t('type')} name={'type'} initialValue={'flash'}> - <Select options={jin10TypeOptions}></Select> - </Form.Item> - <Form.Item label={t('secretKey')} name={'secret_key'}> - <Input></Input> - </Form.Item> - <Form.Item noStyle dependencies={['type']}> - {({ getFieldValue }) => { - const type = getFieldValue('type'); - switch (type) { - case 'flash': - return ( - <> - <Form.Item label={t('flashType')} name={'flash_type'}> - <Select options={jin10FlashTypeOptions}></Select> - </Form.Item> - <Form.Item label={t('contain')} name={'contain'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('filter')} name={'filter'}> - <Input></Input> - </Form.Item> - </> - ); - - case 'calendar': - return ( - <> - <Form.Item label={t('calendarType')} name={'calendar_type'}> - <Select options={jin10CalendarTypeOptions}></Select> - </Form.Item> - <Form.Item - label={t('calendarDatashape')} - name={'calendar_datashape'} - > - <Select options={jin10CalendarDatashapeOptions}></Select> - </Form.Item> - </> - ); - - case 'symbols': - return ( - <> - <Form.Item label={t('symbolsType')} name={'symbols_type'}> - <Select options={jin10SymbolsTypeOptions}></Select> - </Form.Item> - <Form.Item - label={t('symbolsDatatype')} - name={'symbols_datatype'} - > - <Select options={jin10SymbolsDatatypeOptions}></Select> - </Form.Item> - </> - ); - - case 'news': - return ( - <> - <Form.Item label={t('contain')} name={'contain'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('filter')} name={'filter'}> - <Input></Input> - </Form.Item> - </> - ); - - default: - return <></>; - } - }} - </Form.Item> - </Form> - ); -}; - -export default Jin10Form; diff --git a/web/src/pages/flow/form/keyword-extract-form/index.tsx b/web/src/pages/flow/form/keyword-extract-form/index.tsx deleted file mode 100644 index 089df5eab..000000000 --- a/web/src/pages/flow/form/keyword-extract-form/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const KeywordExtractForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect></LLMSelect> - </Form.Item> - <TopNItem initialValue={3}></TopNItem> - </Form> - ); -}; - -export default KeywordExtractForm; diff --git a/web/src/pages/flow/form/message-form/index.less b/web/src/pages/flow/form/message-form/index.less deleted file mode 100644 index 9725f89d2..000000000 --- a/web/src/pages/flow/form/message-form/index.less +++ /dev/null @@ -1,16 +0,0 @@ -.dynamicDeleteButton { - position: relative; - top: 4px; - margin: 0 8px; - color: #999; - font-size: 24px; - cursor: pointer; - transition: all 0.3s; - &:hover { - color: #777; - } - &[disabled] { - cursor: not-allowed; - opacity: 0.5; - } -} diff --git a/web/src/pages/flow/form/message-form/index.tsx b/web/src/pages/flow/form/message-form/index.tsx deleted file mode 100644 index 6040b929d..000000000 --- a/web/src/pages/flow/form/message-form/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Form, Input } from 'antd'; -import { IOperatorForm } from '../../interface'; - -import styles from './index.less'; - -const formItemLayout = { - labelCol: { - sm: { span: 6 }, - }, - wrapperCol: { - sm: { span: 18 }, - }, -}; - -const formItemLayoutWithOutLabel = { - wrapperCol: { - sm: { span: 18, offset: 6 }, - }, -}; - -const MessageForm = ({ onValuesChange, form }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - return ( - <Form - name="basic" - {...formItemLayoutWithOutLabel} - onValuesChange={onValuesChange} - autoComplete="off" - form={form} - > - <Form.List name="messages"> - {(fields, { add, remove }, {}) => ( - <> - {fields.map((field, index) => ( - <Form.Item - {...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)} - label={index === 0 ? t('msg') : ''} - required={false} - key={field.key} - > - <Form.Item - {...field} - validateTrigger={['onChange', 'onBlur']} - rules={[ - { - required: true, - whitespace: true, - message: t('messageMsg'), - }, - ]} - noStyle - > - <Input.TextArea - rows={4} - placeholder={t('messagePlaceholder')} - style={{ width: '80%' }} - /> - </Form.Item> - {fields.length > 1 ? ( - <MinusCircleOutlined - className={styles.dynamicDeleteButton} - onClick={() => remove(field.name)} - /> - ) : null} - </Form.Item> - ))} - <Form.Item> - <Button - type="dashed" - onClick={() => add()} - style={{ width: '80%' }} - icon={<PlusOutlined />} - > - {t('addMessage')} - </Button> - </Form.Item> - </> - )} - </Form.List> - </Form> - ); -}; - -export default MessageForm; diff --git a/web/src/pages/flow/form/pubmed-form/index.tsx b/web/src/pages/flow/form/pubmed-form/index.tsx deleted file mode 100644 index a10962b6e..000000000 --- a/web/src/pages/flow/form/pubmed-form/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const PubMedForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10}></TopNItem> - <Form.Item - label={t('email')} - name={'email'} - tooltip={t('emailTip')} - rules={[{ type: 'email' }]} - > - <Input></Input> - </Form.Item> - </Form> - ); -}; - -export default PubMedForm; diff --git a/web/src/pages/flow/form/qweather-form/index.tsx b/web/src/pages/flow/form/qweather-form/index.tsx deleted file mode 100644 index b69939826..000000000 --- a/web/src/pages/flow/form/qweather-form/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { useCallback, useMemo } from 'react'; -import { - QWeatherLangOptions, - QWeatherTimePeriodOptions, - QWeatherTypeOptions, - QWeatherUserTypeOptions, -} from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const QWeatherForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const qWeatherLangOptions = useMemo(() => { - return QWeatherLangOptions.map((x) => ({ - value: x, - label: t(`qWeatherLangOptions.${x}`), - })); - }, [t]); - - const qWeatherTypeOptions = useMemo(() => { - return QWeatherTypeOptions.map((x) => ({ - value: x, - label: t(`qWeatherTypeOptions.${x}`), - })); - }, [t]); - - const qWeatherUserTypeOptions = useMemo(() => { - return QWeatherUserTypeOptions.map((x) => ({ - value: x, - label: t(`qWeatherUserTypeOptions.${x}`), - })); - }, [t]); - - const getQWeatherTimePeriodOptions = useCallback( - (userType: string) => { - let options = QWeatherTimePeriodOptions; - if (userType === 'free') { - options = options.slice(0, 3); - } - return options.map((x) => ({ - value: x, - label: t(`qWeatherTimePeriodOptions.${x}`), - })); - }, - [t], - ); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item label={t('webApiKey')} name={'web_apikey'}> - <Input></Input> - </Form.Item> - <Form.Item label={t('lang')} name={'lang'}> - <Select options={qWeatherLangOptions}></Select> - </Form.Item> - <Form.Item label={t('type')} name={'type'}> - <Select options={qWeatherTypeOptions}></Select> - </Form.Item> - <Form.Item label={t('userType')} name={'user_type'}> - <Select options={qWeatherUserTypeOptions}></Select> - </Form.Item> - <Form.Item noStyle dependencies={['type', 'user_type']}> - {({ getFieldValue }) => - getFieldValue('type') === 'weather' && ( - <Form.Item label={t('timePeriod')} name={'time_period'}> - <Select - options={getQWeatherTimePeriodOptions( - getFieldValue('user_type'), - )} - ></Select> - </Form.Item> - ) - } - </Form.Item> - </Form> - ); -}; - -export default QWeatherForm; diff --git a/web/src/pages/flow/form/relevant-form/hooks.ts b/web/src/pages/flow/form/relevant-form/hooks.ts deleted file mode 100644 index 5a9340cd3..000000000 --- a/web/src/pages/flow/form/relevant-form/hooks.ts +++ /dev/null @@ -1,52 +0,0 @@ -import pick from 'lodash/pick'; -import { useCallback, useEffect } from 'react'; -import { IOperatorForm } from '../../interface'; -import useGraphStore from '../../store'; - -export const useBuildRelevantOptions = () => { - const nodes = useGraphStore((state) => state.nodes); - - const buildRelevantOptions = useCallback( - (toList: string[]) => { - return nodes - .filter( - (x) => !toList.some((y) => y === x.id), // filter out selected values ​​in other to fields from the current drop-down box options - ) - .map((x) => ({ label: x.data.name, value: x.id })); - }, - [nodes], - ); - - return buildRelevantOptions; -}; - -// const getTargetOfEdge = (edges: Edge[], sourceHandle: string) => -// edges.find((x) => x.sourceHandle === sourceHandle)?.target; - -/** - * 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 edges = useGraphStore((state) => state.edges); - const getNode = useGraphStore((state) => state.getNode); - const node = getNode(nodeId); - - const watchFormChanges = useCallback(() => { - if (node) { - form?.setFieldsValue(pick(node, ['yes', 'no'])); - } - }, [node, form]); - - // const watchConnectionChanges = useCallback(() => { - // const edgeList = edges.filter((x) => x.source === nodeId); - // const yes = getTargetOfEdge(edgeList, 'yes'); - // const no = getTargetOfEdge(edgeList, 'no'); - // form?.setFieldsValue({ yes, no }); - // }, [edges, nodeId, form]); - - useEffect(() => { - watchFormChanges(); - }, [watchFormChanges]); -}; diff --git a/web/src/pages/flow/form/relevant-form/index.tsx b/web/src/pages/flow/form/relevant-form/index.tsx deleted file mode 100644 index e2366f6f0..000000000 --- a/web/src/pages/flow/form/relevant-form/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { Operator } from '../../constant'; -import { useBuildFormSelectOptions } from '../../form-hooks'; -import { IOperatorForm } from '../../interface'; -import { useWatchConnectionChanges } from './hooks'; - -const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - const buildRelevantOptions = useBuildFormSelectOptions( - Operator.Relevant, - node?.id, - ); - useWatchConnectionChanges({ nodeId: node?.id, form }); - - return ( - <Form - name="basic" - labelCol={{ span: 4 }} - wrapperCol={{ span: 20 }} - onValuesChange={onValuesChange} - autoComplete="off" - form={form} - > - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect></LLMSelect> - </Form.Item> - <Form.Item label={t('yes')} name={'yes'}> - <Select - allowClear - options={buildRelevantOptions([form?.getFieldValue('no')])} - /> - </Form.Item> - <Form.Item label={t('no')} name={'no'}> - <Select - allowClear - options={buildRelevantOptions([form?.getFieldValue('yes')])} - /> - </Form.Item> - </Form> - ); -}; - -export default RelevantForm; diff --git a/web/src/pages/flow/form/retrieval-form/index.tsx b/web/src/pages/flow/form/retrieval-form/index.tsx deleted file mode 100644 index fa3da7131..000000000 --- a/web/src/pages/flow/form/retrieval-form/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import KnowledgeBaseItem from '@/components/knowledge-base-item'; -import Rerank from '@/components/rerank'; -import SimilaritySlider from '@/components/similarity-slider'; -import { TavilyItem } from '@/components/tavily-item'; -import TopNItem from '@/components/top-n-item'; -import { UseKnowledgeGraphItem } from '@/components/use-knowledge-graph-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import type { FormProps } from 'antd'; -import { Form, Input } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -type FieldType = { - top_n?: number; -}; - -const onFinish: FormProps<FieldType>['onFinish'] = (values) => { - console.log('Success:', values); -}; - -const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => { - console.log('Failed:', errorInfo); -}; - -const RetrievalForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - return ( - <Form - name="basic" - onFinish={onFinish} - onFinishFailed={onFinishFailed} - autoComplete="off" - onValuesChange={onValuesChange} - form={form} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <SimilaritySlider - isTooltipShown - vectorSimilarityWeightName="keywords_similarity_weight" - ></SimilaritySlider> - <TopNItem></TopNItem> - <Rerank></Rerank> - <TavilyItem name={'tavily_api_key'}></TavilyItem> - <UseKnowledgeGraphItem filedName={'use_kg'}></UseKnowledgeGraphItem> - <KnowledgeBaseItem - tooltipText={t('knowledgeBasesTip')} - ></KnowledgeBaseItem> - <DynamicInputVariable - name={'kb_vars'} - node={node} - title={t('knowledgeBaseVars')} - ></DynamicInputVariable> - <Form.Item - name={'empty_response'} - label={t('emptyResponse', { keyPrefix: 'chat' })} - tooltip={t('emptyResponseTip', { keyPrefix: 'chat' })} - > - <Input.TextArea placeholder="" rows={4} /> - </Form.Item> - </Form> - ); -}; - -export default RetrievalForm; diff --git a/web/src/pages/flow/form/rewrite-question-form/index.tsx b/web/src/pages/flow/form/rewrite-question-form/index.tsx deleted file mode 100644 index c2b2e8db8..000000000 --- a/web/src/pages/flow/form/rewrite-question-form/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import LLMSelect from '@/components/llm-select'; -import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { GoogleLanguageOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; - -const RewriteQuestionForm = ({ onValuesChange, form }: IOperatorForm) => { - const { t } = useTranslate('chat'); - - return ( - <Form - name="basic" - labelCol={{ span: 8 }} - wrapperCol={{ span: 16 }} - onValuesChange={onValuesChange} - autoComplete="off" - form={form} - > - <Form.Item - name={'llm_id'} - label={t('model', { keyPrefix: 'chat' })} - tooltip={t('modelTip', { keyPrefix: 'chat' })} - > - <LLMSelect></LLMSelect> - </Form.Item> - <Form.Item - label={t('language')} - name={'language'} - tooltip={t('languageTip')} - > - <Select options={GoogleLanguageOptions} allowClear={true}></Select> - </Form.Item> - <MessageHistoryWindowSizeItem - initialValue={6} - ></MessageHistoryWindowSizeItem> - </Form> - ); -}; - -export default RewriteQuestionForm; diff --git a/web/src/pages/flow/form/switch-form/index.less b/web/src/pages/flow/form/switch-form/index.less deleted file mode 100644 index c5e2c7fe8..000000000 --- a/web/src/pages/flow/form/switch-form/index.less +++ /dev/null @@ -1,21 +0,0 @@ -@lightBackgroundColor: rgba(150, 150, 150, 0.07); -@darkBackgroundColor: rgba(150, 150, 150, 0.12); - -.caseCard { - background-color: @lightBackgroundColor; -} - -.conditionCard { - background-color: @darkBackgroundColor; -} - -.elseCase { - background-color: @lightBackgroundColor; - padding: 12px; - border-radius: 8px; -} - -.addButton { - color: rgb(22, 119, 255); - font-weight: 600; -} diff --git a/web/src/pages/flow/form/switch-form/index.tsx b/web/src/pages/flow/form/switch-form/index.tsx deleted file mode 100644 index 222a2baa3..000000000 --- a/web/src/pages/flow/form/switch-form/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { CloseOutlined } from '@ant-design/icons'; -import { Button, Card, Divider, Form, Input, Select } from 'antd'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Operator, - SwitchElseTo, - SwitchLogicOperatorOptions, - SwitchOperatorOptions, -} from '../../constant'; -import { useBuildFormSelectOptions } from '../../form-hooks'; -import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; -import { IOperatorForm } from '../../interface'; -import { getOtherFieldValues } from '../../utils'; - -import { ISwitchForm } from '@/interfaces/database/flow'; -import styles from './index.less'; - -const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => { - const { t } = useTranslation(); - const buildCategorizeToOptions = useBuildFormSelectOptions( - Operator.Switch, - node?.id, - ); - - const getSelectedConditionTos = () => { - const conditions: ISwitchForm['conditions'] = - form?.getFieldValue('conditions'); - - return conditions?.filter((x) => !!x).map((x) => x?.to) ?? []; - }; - - const switchOperatorOptions = useMemo(() => { - return SwitchOperatorOptions.map((x) => ({ - value: x.value, - label: t(`flow.switchOperatorOptions.${x.label}`), - })); - }, [t]); - - const switchLogicOperatorOptions = useMemo(() => { - return SwitchLogicOperatorOptions.map((x) => ({ - value: x, - label: t(`flow.switchLogicOperatorOptions.${x}`), - })); - }, [t]); - - const componentIdOptions = useBuildComponentIdSelectOptions( - node?.id, - node?.parentId, - ); - - return ( - <Form - form={form} - name="dynamic_form_complex" - autoComplete="off" - initialValues={{ conditions: [{}] }} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <Form.List name="conditions"> - {(fields, { add, remove }) => ( - <div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}> - {fields.map((field) => { - return ( - <Card - size="small" - title={`Case ${field.name + 1}`} - key={field.key} - className={styles.caseCard} - extra={ - <CloseOutlined - onClick={() => { - remove(field.name); - }} - /> - } - > - <Form.Item noStyle dependencies={[field.name, 'items']}> - {({ getFieldValue }) => - getFieldValue(['conditions', field.name, 'items']) - ?.length > 1 && ( - <Form.Item - label={t('flow.logicalOperator')} - name={[field.name, 'logical_operator']} - > - <Select options={switchLogicOperatorOptions} /> - </Form.Item> - ) - } - </Form.Item> - <Form.Item - label={t('flow.nextStep')} - name={[field.name, 'to']} - > - <Select - allowClear - options={buildCategorizeToOptions([ - form?.getFieldValue(SwitchElseTo), - ...getOtherFieldValues( - form!, - 'conditions', - field, - 'to', - ), - ])} - /> - </Form.Item> - <Form.Item label="Condition"> - <Form.List name={[field.name, 'items']}> - {(subFields, subOpt) => ( - <div - style={{ - display: 'flex', - flexDirection: 'column', - rowGap: 16, - }} - > - {subFields.map((subField) => ( - <Card - key={subField.key} - title={null} - size="small" - className={styles.conditionCard} - bordered - extra={ - <CloseOutlined - onClick={() => { - subOpt.remove(subField.name); - }} - /> - } - > - <Form.Item - label={t('flow.componentId')} - name={[subField.name, 'cpn_id']} - > - <Select - placeholder={t('flow.componentId')} - options={componentIdOptions} - /> - </Form.Item> - <Form.Item - label={t('flow.operator')} - name={[subField.name, 'operator']} - > - <Select - placeholder={t('flow.operator')} - options={switchOperatorOptions} - /> - </Form.Item> - <Form.Item - label={t('flow.value')} - name={[subField.name, 'value']} - > - <Input placeholder={t('flow.value')} /> - </Form.Item> - </Card> - ))} - <Button - onClick={() => { - form?.setFieldValue( - ['conditions', field.name, 'logical_operator'], - SwitchLogicOperatorOptions[0], - ); - subOpt.add({ - operator: SwitchOperatorOptions[0].value, - }); - }} - block - className={styles.addButton} - > - + Add Condition - </Button> - </div> - )} - </Form.List> - </Form.Item> - </Card> - ); - })} - - <Button onClick={() => add()} block className={styles.addButton}> - + Add Case - </Button> - </div> - )} - </Form.List> - <Divider /> - <Form.Item - label={'ELSE'} - name={[SwitchElseTo]} - className={styles.elseCase} - > - <Select - allowClear - options={buildCategorizeToOptions(getSelectedConditionTos())} - /> - </Form.Item> - </Form> - ); -}; - -export default SwitchForm; diff --git a/web/src/pages/flow/form/template-form/index.tsx b/web/src/pages/flow/form/template-form/index.tsx deleted file mode 100644 index ddf5c7883..000000000 --- a/web/src/pages/flow/form/template-form/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { PromptEditor } from '@/components/prompt-editor'; -import { Form } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { IOperatorForm } from '../../interface'; - -const TemplateForm = ({ onValuesChange, form }: IOperatorForm) => { - const { t } = useTranslation(); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <Form.Item name={['content']} label={t('flow.content')}> - <PromptEditor></PromptEditor> - </Form.Item> - </Form> - ); -}; - -export default TemplateForm; diff --git a/web/src/pages/flow/form/tushare-form/index.tsx b/web/src/pages/flow/form/tushare-form/index.tsx deleted file mode 100644 index 01b11c220..000000000 --- a/web/src/pages/flow/form/tushare-form/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { DatePicker, DatePickerProps, Form, Input, Select } from 'antd'; -import dayjs from 'dayjs'; -import { useCallback, useMemo } from 'react'; -import { TuShareSrcOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const DateTimePicker = ({ - onChange, - value, -}: { - onChange?: (val: number | undefined) => void; - value?: number | undefined; -}) => { - const handleChange: DatePickerProps['onChange'] = useCallback( - (val: any) => { - const nextVal = val?.format('YYYY-MM-DD HH:mm:ss'); - onChange?.(nextVal ? nextVal : undefined); - }, - [onChange], - ); - // The value needs to be converted into a string and saved to the backend - const nextValue = useMemo(() => { - if (value) { - return dayjs(value); - } - return undefined; - }, [value]); - - return ( - <DatePicker - showTime - format="YYYY-MM-DD HH:mm:ss" - onChange={handleChange} - value={nextValue} - /> - ); -}; - -const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const tuShareSrcOptions = useMemo(() => { - return TuShareSrcOptions.map((x) => ({ - value: x, - label: t(`tuShareSrcOptions.${x}`), - })); - }, [t]); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item - label={t('token')} - name={'token'} - tooltip={'Get from https://tushare.pro/'} - > - <Input></Input> - </Form.Item> - <Form.Item label={t('src')} name={'src'}> - <Select options={tuShareSrcOptions}></Select> - </Form.Item> - <Form.Item label={t('startDate')} name={'start_date'}> - <DateTimePicker /> - </Form.Item> - <Form.Item label={t('endDate')} name={'end_date'}> - <DateTimePicker /> - </Form.Item> - <Form.Item label={t('keyword')} name={'keyword'}> - <Input></Input> - </Form.Item> - </Form> - ); -}; - -export default TuShareForm; diff --git a/web/src/pages/flow/form/wencai-form/index.tsx b/web/src/pages/flow/form/wencai-form/index.tsx deleted file mode 100644 index 7c44c182f..000000000 --- a/web/src/pages/flow/form/wencai-form/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { useMemo } from 'react'; -import { WenCaiQueryTypeOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const WenCaiForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const wenCaiQueryTypeOptions = useMemo(() => { - return WenCaiQueryTypeOptions.map((x) => ({ - value: x, - label: t(`wenCaiQueryTypeOptions.${x}`), - })); - }, [t]); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={20} max={99}></TopNItem> - <Form.Item label={t('queryType')} name={'query_type'}> - <Select options={wenCaiQueryTypeOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default WenCaiForm; diff --git a/web/src/pages/flow/form/wikipedia-form/index.tsx b/web/src/pages/flow/form/wikipedia-form/index.tsx deleted file mode 100644 index 9e28bf21d..000000000 --- a/web/src/pages/flow/form/wikipedia-form/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import TopNItem from '@/components/top-n-item'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Select } from 'antd'; -import { LanguageOptions } from '../../constant'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const WikipediaForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('common'); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <TopNItem initialValue={10}></TopNItem> - <Form.Item label={t('language')} name={'language'}> - <Select options={LanguageOptions}></Select> - </Form.Item> - </Form> - ); -}; - -export default WikipediaForm; diff --git a/web/src/pages/flow/form/yahoo-finance-form/index.tsx b/web/src/pages/flow/form/yahoo-finance-form/index.tsx deleted file mode 100644 index ce7a3e7d2..000000000 --- a/web/src/pages/flow/form/yahoo-finance-form/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Switch } from 'antd'; -import { IOperatorForm } from '../../interface'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const YahooFinanceForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - return ( - <Form - name="basic" - autoComplete="off" - form={form} - onValuesChange={onValuesChange} - layout={'vertical'} - > - <DynamicInputVariable node={node}></DynamicInputVariable> - <Form.Item label={t('info')} name={'info'}> - <Switch></Switch> - </Form.Item> - <Form.Item label={t('history')} name={'history'}> - <Switch></Switch> - </Form.Item> - <Form.Item label={t('financials')} name={'financials'}> - <Switch></Switch> - </Form.Item> - <Form.Item label={t('balanceSheet')} name={'balance_sheet'}> - <Switch></Switch> - </Form.Item> - <Form.Item label={t('cashFlowStatement')} name={'cash_flow_statement'}> - <Switch></Switch> - </Form.Item> - <Form.Item label={t('news')} name={'news'}> - <Switch></Switch> - </Form.Item> - </Form> - ); -}; - -export default YahooFinanceForm; diff --git a/web/src/pages/flow/header/index.less b/web/src/pages/flow/header/index.less deleted file mode 100644 index 714b24d4f..000000000 --- a/web/src/pages/flow/header/index.less +++ /dev/null @@ -1,10 +0,0 @@ -.flowHeader { - padding: 10px 20px; -} -.hideRibbon { - display: none !important; -} - -.ribbon { - top: 4px; -} diff --git a/web/src/pages/flow/header/index.tsx b/web/src/pages/flow/header/index.tsx deleted file mode 100644 index 41626387f..000000000 --- a/web/src/pages/flow/header/index.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import EmbedModal from '@/components/api-service/embed-modal'; -import { useShowEmbedModal } from '@/components/api-service/hooks'; -import { SharedFrom } from '@/constants/chat'; -import { useTranslate } from '@/hooks/common-hooks'; -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; -import { ArrowLeftOutlined } from '@ant-design/icons'; -import { Badge, Button, Flex, Space } from 'antd'; -import classNames from 'classnames'; -import { useCallback } from 'react'; -import { Link, useParams } from 'umi'; -import { FlowSettingModal, useFlowSettingModal } from '../flow-setting'; -import { - useGetBeginNodeDataQuery, - useGetBeginNodeDataQueryIsSafe, -} from '../hooks/use-get-begin-query'; -import { - useSaveGraph, - useSaveGraphBeforeOpeningDebugDrawer, - useWatchAgentChange, -} from '../hooks/use-save-graph'; -import { BeginQuery } from '../interface'; - -import { - HistoryVersionModal, - useHistoryVersionModal, -} from '../history-version-modal'; -import styles from './index.less'; - -interface IProps { - showChatDrawer(): void; - chatDrawerVisible: boolean; -} - -const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { - const { saveGraph } = useSaveGraph(); - const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); - const { data: userInfo } = useFetchUserInfo(); - - const { data } = useFetchFlow(); - const { t } = useTranslate('flow'); - const { id } = useParams(); - const time = useWatchAgentChange(chatDrawerVisible); - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const { showEmbedModal, hideEmbedModal, embedVisible, beta } = - useShowEmbedModal(); - const { setVisibleSettingMModal, visibleSettingModal } = - useFlowSettingModal(); - const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe(); - const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } = - useHistoryVersionModal(); - const handleShowEmbedModal = useCallback(() => { - showEmbedModal(); - }, [showEmbedModal]); - - const handleRunAgent = useCallback(() => { - const query: BeginQuery[] = getBeginNodeDataQuery(); - if (query.length > 0) { - showChatDrawer(); - } else { - handleRun(); - } - }, [getBeginNodeDataQuery, handleRun, showChatDrawer]); - - const showSetting = useCallback(() => { - setVisibleSettingMModal(true); - }, [setVisibleSettingMModal]); - - const showListVersion = useCallback(() => { - setVisibleHistoryVersionModal(true); - }, [setVisibleHistoryVersionModal]); - return ( - <> - <Flex - align="center" - justify={'space-between'} - gap={'large'} - className={styles.flowHeader} - > - <Badge.Ribbon - text={data?.nickname} - style={{ marginRight: -data?.nickname?.length * 5 }} - color={userInfo?.nickname === data?.nickname ? '#1677ff' : 'pink'} - className={classNames(styles.ribbon, { - [styles.hideRibbon]: data.permission !== 'team', - })} - > - <Space className={styles.headerTitle} size={'large'}> - <Link to={`/flow`}> - <ArrowLeftOutlined /> - </Link> - <div className="flex flex-col"> - <span className="font-semibold text-[18px]">{data.title}</span> - <span className="font-normal text-sm"> - {t('autosaved')} {time} - </span> - </div> - </Space> - </Badge.Ribbon> - <Space size={'large'}> - <Button - disabled={userInfo.nickname !== data.nickname} - onClick={handleRunAgent} - > - <b>{t('run')}</b> - </Button> - <Button - disabled={userInfo.nickname !== data.nickname} - type="primary" - onClick={() => saveGraph()} - > - <b>{t('save')}</b> - </Button> - <Button - type="primary" - onClick={handleShowEmbedModal} - disabled={ - !isBeginNodeDataQuerySafe || userInfo.nickname !== data.nickname - } - > - <b>{t('embedIntoSite', { keyPrefix: 'common' })}</b> - </Button> - <Button - disabled={userInfo.nickname !== data.nickname} - type="primary" - onClick={showSetting} - > - <b>{t('setting')}</b> - </Button> - <Button type="primary" onClick={showListVersion}> - <b>{t('historyversion')}</b> - </Button> - </Space> - </Flex> - {embedVisible && ( - <EmbedModal - visible={embedVisible} - hideModal={hideEmbedModal} - token={id!} - form={SharedFrom.Agent} - beta={beta} - isAgent - ></EmbedModal> - )} - {visibleSettingModal && ( - <FlowSettingModal - id={id || ''} - visible={visibleSettingModal} - hideModal={() => setVisibleSettingMModal(false)} - ></FlowSettingModal> - )} - {visibleHistoryVersionModal && ( - <HistoryVersionModal - id={id || ''} - visible={visibleHistoryVersionModal} - hideModal={() => setVisibleHistoryVersionModal(false)} - ></HistoryVersionModal> - )} - </> - ); -}; - -export default FlowHeader; diff --git a/web/src/pages/flow/headhunter_zh.json b/web/src/pages/flow/headhunter_zh.json deleted file mode 100644 index 6445bb31f..000000000 --- a/web/src/pages/flow/headhunter_zh.json +++ /dev/null @@ -1,397 +0,0 @@ -{ - "edges": [ - { - "id": "85fcec8f-7663-4221-99fa-1d41a6358cc5", - "label": "", - "source": "begin", - "target": "answer:0" - }, - { - "id": "36bf941f-b8df-46e7-87d3-5ce51b5378fb", - "label": "", - "source": "message:reject", - "target": "answer:0" - }, - { - "id": "e5cca6d3-ffd2-4824-9471-3fe83891dd73", - "label": "", - "source": "answer:0", - "target": "categorize:0" - }, - { - "id": "8a39ca91-5b04-4970-ad64-968797eb62a2", - "label": "", - "source": "categorize:0", - "target": "message:introduction", - "sourceHandle": "interested" - }, - { - "id": "675752ca-6c81-432b-a6b1-eaca4a6c3a96", - "label": "", - "source": "categorize:0", - "target": "generate:casual", - "sourceHandle": "casual" - }, - { - "id": "cc1628b4-b1c3-4941-ab19-3777993e094a", - "label": "", - "source": "categorize:0", - "target": "message:reject", - "sourceHandle": "answer" - }, - { - "id": "8a11d81f-820a-41df-a47b-daec65c5fb61", - "label": "", - "source": "categorize:0", - "target": "retrieval:0", - "sourceHandle": "about_job" - }, - { - "id": "3afa4bbd-1151-4452-8f58-9cc03496c2e7", - "label": "", - "source": "message:introduction", - "target": "answer:1" - }, - { - "id": "6e705ec1-d7c2-4278-8ddd-03ed05ec2973", - "label": "", - "source": "generate:aboutJob", - "target": "answer:1" - }, - { - "id": "9c6b9bfc-e8ff-4903-8479-67f89f159b55", - "label": "", - "source": "generate:casual", - "target": "answer:1" - }, - { - "id": "ddefbff1-2cca-4251-bc34-2f3cea7a5593", - "label": "", - "source": "generate:get_wechat", - "target": "answer:1" - }, - { - "id": "13e14da1-3865-4539-af77-4e9627e67273", - "label": "", - "source": "generate:nowechat", - "target": "answer:1" - }, - { - "id": "de6b8407-8af3-4450-9f74-6f4e6b99da9f", - "label": "", - "source": "answer:1", - "target": "categorize:1" - }, - { - "id": "36f0f440-129b-4073-8d1b-55eaf452ebd2", - "label": "", - "source": "categorize:1", - "target": "retrieval:0", - "sourceHandle": "about_job" - }, - { - "id": "72d63756-f054-488f-adfd-b3a376047fa2", - "label": "", - "source": "categorize:1", - "target": "generate:casual", - "sourceHandle": "casual" - }, - { - "id": "5ee58522-246c-427a-806b-e82fccdd7dc6", - "label": "", - "source": "categorize:1", - "target": "generate:get_wechat", - "sourceHandle": "wechat" - }, - { - "id": "994cbb21-9dad-40af-aee3-9d4b8199a843", - "label": "", - "source": "categorize:1", - "target": "generate:nowechat", - "sourceHandle": "giveup" - }, - { - "id": "5b864e95-f44e-428b-8a2e-323c3bed0701", - "label": "", - "source": "retrieval:0", - "target": "generate:aboutJob" - }, - { - "id": "e882800b-077e-445a-a544-e147f7a9911c", - "label": "", - "source": "relevant:0", - "target": "generate:aboutJob" - } - ], - "nodes": [ - { - "id": "begin", - "type": "beginNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Begin", - "name": "TrueBagsAllow", - "form": { - "prologue": "您好!我是AGI方向的猎头,了解到您是这方面的大佬,然后冒昧的就联系到您。这边有个机会想和您分享,RAGFlow正在招聘您这个岗位的资深的工程师不知道您那边是不是感兴趣?" - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "answer:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Answer", - "name": "YoungWeeksArgue", - "form": {} - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "categorize:0", - "type": "categorizeNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Categorize", - "name": "FancyBooksLose", - "form": { - "llm_id": "deepseek-chat", - "category_description": { - "about_job": { - "description": "该问题关于职位本身或公司的信息。", - "examples": "什么岗位?\n汇报对象是谁?\n公司多少人?\n公司有啥产品?\n具体工作内容是啥?\n地点哪里?\n双休吗?", - "to": "retrieval:0" - }, - "casual": { - "description": "该问题不关于职位本身或公司的信息,属于闲聊。", - "examples": "你好\n好久不见\n你男的女的?\n你是猴子派来的救兵吗?\n上午开会了?\n你叫啥?\n最近市场如何?生意好做吗?", - "to": "generate:casual" - }, - "interested": { - "description": "该回答表示他对于该职位感兴趣。", - "examples": "嗯\n说吧\n说说看\n还好吧\n是的\n哦\nyes\n具体说说", - "to": "message:introduction" - }, - "answer": { - "description": "该回答表示他对于该职位不感兴趣,或感觉受到骚扰。", - "examples": "不需要\n不感兴趣\n暂时不看\n不要\nno\n我已经不干这个了\n我不是这个方向的", - "to": "message:reject" - } - } - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "message:introduction", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Message", - "name": "CyanMangosStudy", - "form": { - "messages": [ - "我简单介绍以下:\nRAGFlow 是一款基于深度文档理解构建的开源 RAG(Retrieval-Augmented Generation)引擎。RAGFlow 可以为各种规模的企业及个人提供一套精简的 RAG 工作流程,结合大语言模型(LLM)针对用户各类不同的复杂格式数据提供可靠的问答以及有理有据的引用。https://github.com/infiniflow/ragflow\n您那边还有什么要了解的?" - ] - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "answer:1", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Answer", - "name": "SwiftEyesReply", - "form": {} - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "categorize:1", - "type": "categorizeNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Categorize", - "name": "KhakiEmusGive", - "form": { - "llm_id": "deepseek-chat", - "category_description": { - "about_job": { - "description": "该问题关于职位本身或公司的信息。", - "examples": "什么岗位?\n汇报对象是谁?\n公司多少人?\n公司有啥产品?\n具体工作内容是啥?\n地点哪里?\n双休吗?", - "to": "retrieval:0" - }, - "casual": { - "description": "该问题不关于职位本身或公司的信息,属于闲聊。", - "examples": "你好\n好久不见\n你男的女的?\n你是猴子派来的救兵吗?\n上午开会了?\n你叫啥?\n最近市场如何?生意好做吗?", - "to": "generate:casual" - }, - "wechat": { - "description": "该回答表示他愿意加微信,或者已经报了微信号。", - "examples": "嗯\n可以\n是的\n哦\nyes\n15002333453\nwindblow_2231", - "to": "generate:get_wechat" - }, - "giveup": { - "description": "该回答表示他不愿意加微信。", - "examples": "不需要\n不感兴趣\n暂时不看\n不要\nno\n不方便\n不知道还要加我微信", - "to": "generate:nowechat" - } - }, - "message_history_window_size": 8 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:casual", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "BigToesSee", - "form": { - "llm_id": "deepseek-chat", - "prompt": "你是AGI方向的猎头,现在候选人的聊了和职位无关的话题,请耐心的回应候选人,并将话题往该AGI的职位上带,最好能要到候选人微信号以便后面保持联系。", - "temperature": 0.9, - "message_history_window_size": 12, - "cite": false - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "retrieval:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Retrieval", - "name": "SilentWormsOpen", - "form": { - "similarity_threshold": 0.2, - "keywords_similarity_weight": 0.3, - "top_n": 6, - "top_k": 1024, - "rerank_id": "BAAI/bge-reranker-v2-m3", - "kb_ids": ["869a236818b811ef91dffa163e197198"] - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:aboutJob", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "ItchySuitsWait", - "form": { - "llm_id": "deepseek-chat", - "prompt": "你是AGI方向的猎头,候选人问了有关职位或公司的问题,你根据以下职位信息回答。如果职位信息中不包含候选人的问题就回答不清楚、不知道、有待确认等。回答完后引导候选人加微信号,如:\n - 方便加一下微信吗,我把JD发您看看?\n - 微信号多少,我把详细职位JD发您?\n 职位信息如下:\n {input}\n 职位信息如上。", - "temperature": 0.02 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:get_wechat", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "PlentyCasesPoke", - "form": { - "llm_id": "deepseek-chat", - "prompt": "你是AGI方向的猎头,候选人表示不反感加微信,如果对方已经报了微信号,表示感谢和信任并表示马上会加上;如果没有,则问对方微信号多少。你的微信号是weixin_kevin,E-mail是kkk@ragflow.com。说话不要重复。不要总是您好。", - "temperature": 0.1, - "message_history_window_size": 12, - "cite": false - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:nowechat", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "SharpEggsLove", - "form": { - "llm_id": "deepseek-chat", - "prompt": "你是AGI方向的猎头,当你提出加微信时对方表示拒绝。你需要耐心礼貌的回应候选人,表示对于保护隐私信息给予理解,也可以询问他对该职位的看法和顾虑。并在恰当的时机再次询问微信联系方式。也可以鼓励候选人主动与你取得联系。你的微信号是weixin_kevin,E-mail是kkk@ragflow.com。说话不要重复。不要总是您好。", - "temperature": 0.1, - "message_history_window_size": 12, - "cite": false - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "message:reject", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Message", - "name": "MetalWolvesBeam", - "form": { - "messages": [ - "好的,祝您生活愉快,工作顺利。", - "哦,好的,感谢您宝贵的时间!" - ] - } - }, - "sourcePosition": "left", - "targetPosition": "right" - } - ] -} diff --git a/web/src/pages/flow/history-version-modal/index.tsx b/web/src/pages/flow/history-version-modal/index.tsx deleted file mode 100644 index 96213c694..000000000 --- a/web/src/pages/flow/history-version-modal/index.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { useFetchListVersion, useFetchVersion } from '@/hooks/flow-hooks'; -import { - Background, - ConnectionMode, - ReactFlow, - ReactFlowProvider, -} from '@xyflow/react'; -import { Card, Col, Empty, List, Modal, Row, Spin, Typography } from 'antd'; -import React, { useState } from 'react'; -import { nodeTypes } from '../canvas'; - -export function useHistoryVersionModal() { - const [visibleHistoryVersionModal, setVisibleHistoryVersionModal] = - React.useState(false); - - return { - visibleHistoryVersionModal, - setVisibleHistoryVersionModal, - }; -} - -type HistoryVersionModalProps = { - visible: boolean; - hideModal: () => void; - id: string; -}; - -export function HistoryVersionModal({ - visible, - hideModal, - id, -}: HistoryVersionModalProps) { - const { t } = useTranslate('flow'); - const { data, loading } = useFetchListVersion(id); - const [selectedVersion, setSelectedVersion] = useState<any>(null); - const { data: flow, loading: loadingVersion } = useFetchVersion( - selectedVersion?.id, - ); - - React.useEffect(() => { - if (!loading && data?.length > 0 && !selectedVersion) { - setSelectedVersion(data[0]); - } - }, [data, loading, selectedVersion]); - - const downloadfile = React.useCallback( - function (e: any) { - e.stopPropagation(); - console.log('Restore version:', selectedVersion); - // Create a JSON blob and trigger download - const jsonContent = JSON.stringify(flow?.dsl.graph, null, 2); - const blob = new Blob([jsonContent], { - type: 'application/json', - }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${selectedVersion.filename || 'flow-version'}-${selectedVersion.id}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }, - [selectedVersion, flow?.dsl], - ); - return ( - <React.Fragment> - <Modal - title={t('historyversion')} - open={visible} - width={'80vw'} - onCancel={hideModal} - footer={null} - getContainer={() => document.body} - > - <Row gutter={16} style={{ height: '60vh' }}> - <Col span={10} style={{ height: '100%', overflowY: 'auto' }}> - {loading && <Spin />} - {!loading && data.length === 0 && ( - <Empty description="No versions found" /> - )} - {!loading && data.length > 0 && ( - <List - itemLayout="horizontal" - dataSource={data} - pagination={{ - pageSize: 5, - simple: true, - }} - renderItem={(item) => ( - <List.Item - key={item.id} - onClick={(e) => { - e.stopPropagation(); - setSelectedVersion(item); - }} - style={{ - cursor: 'pointer', - background: - selectedVersion?.id === item.id ? '#f0f5ff' : 'inherit', - padding: '8px 12px', - borderRadius: '4px', - }} - > - <List.Item.Meta - title={`${t('filename')}: ${item.title || '-'}`} - description={item.created_at} - /> - </List.Item> - )} - /> - )} - </Col> - - {/* Right panel - Version details */} - <Col span={14} style={{ height: '100%', overflowY: 'auto' }}> - {selectedVersion ? ( - <Card title={t('version.details')} bordered={false}> - <Row gutter={[16, 16]}> - {/* Add actions for the selected version (restore, download, etc.) */} - <Col span={24}> - <div style={{ textAlign: 'right' }}> - <Typography.Link onClick={downloadfile}> - {t('version.download')} - </Typography.Link> - </div> - </Col> - </Row> - <Typography.Title level={4}> - {selectedVersion.title || '-'} - </Typography.Title> - - <Typography.Text - type="secondary" - style={{ display: 'block', marginBottom: 16 }} - > - {t('version.created')}: {selectedVersion.create_date} - </Typography.Text> - - {/*render dsl form api*/} - {loadingVersion && <Spin />} - {!loadingVersion && flow?.dsl && ( - <ReactFlowProvider key={`flow-${selectedVersion.id}`}> - <div - style={{ - height: '400px', - position: 'relative', - zIndex: 0, - }} - > - <ReactFlow - connectionMode={ConnectionMode.Loose} - nodes={flow?.dsl.graph?.nodes || []} - edges={ - flow?.dsl.graph?.edges.flatMap((x) => ({ - ...x, - type: 'default', - })) || [] - } - fitView - nodeTypes={nodeTypes} - edgeTypes={{}} - zoomOnScroll={true} - panOnDrag={true} - zoomOnDoubleClick={false} - preventScrolling={true} - minZoom={0.1} - > - <Background /> - </ReactFlow> - </div> - </ReactFlowProvider> - )} - </Card> - ) : ( - <Empty description={t('version.select')} /> - )} - </Col> - </Row> - </Modal> - </React.Fragment> - ); -} diff --git a/web/src/pages/flow/hooks.tsx b/web/src/pages/flow/hooks.tsx deleted file mode 100644 index 9467e0b6f..000000000 --- a/web/src/pages/flow/hooks.tsx +++ /dev/null @@ -1,572 +0,0 @@ -import { - Connection, - Edge, - Node, - Position, - ReactFlowInstance, -} from '@xyflow/react'; -import React, { - ChangeEvent, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -// import { shallow } from 'zustand/shallow'; -import { settledModelVariableMap } from '@/constants/knowledge'; -import { useFetchModelId } from '@/hooks/logic-hooks'; -import { - ICategorizeForm, - IRelevantForm, - ISwitchForm, - RAGFlowNodeType, -} from '@/interfaces/database/flow'; -import { setChatVariableEnabledFieldValuePage } from '@/utils/chat'; -import { message } from 'antd'; -import { humanId } from 'human-id'; -import { get, lowerFirst } from 'lodash'; -import trim from 'lodash/trim'; -import { useTranslation } from 'react-i18next'; -import { v4 as uuid } from 'uuid'; -import { - NodeMap, - Operator, - RestrictedUpstreamMap, - SwitchElseTo, - initialAkShareValues, - initialArXivValues, - initialBaiduFanyiValues, - initialBaiduValues, - initialBeginValues, - initialBingValues, - initialCategorizeValues, - initialCodeValues, - initialConcentratorValues, - initialCrawlerValues, - initialDeepLValues, - initialDuckValues, - initialEmailValues, - initialExeSqlValues, - initialGenerateValues, - initialGithubValues, - initialGoogleScholarValues, - initialGoogleValues, - initialInvokeValues, - initialIterationValues, - initialJin10Values, - initialKeywordExtractValues, - initialMessageValues, - initialNoteValues, - initialPubMedValues, - initialQWeatherValues, - initialRelevantValues, - initialRetrievalValues, - initialRewriteQuestionValues, - initialSwitchValues, - initialTemplateValues, - initialTuShareValues, - initialWenCaiValues, - initialWikipediaValues, - initialYahooFinanceValues, -} from './constant'; -import useGraphStore, { RFState } from './store'; -import { - generateNodeNamesWithIncreasingIndex, - generateSwitchHandleText, - 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, -}); - -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.Generate]: { ...initialGenerateValues, llm_id: llmId }, - [Operator.Answer]: {}, - [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.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.Template]: initialTemplateValues, - [Operator.Email]: initialEmailValues, - [Operator.Iteration]: initialIterationValues, - [Operator.IterationStart]: initialIterationValues, - [Operator.Code]: initialCodeValues, - }; - }, [llmId]); - - const initializeOperatorParams = useCallback( - (operatorName: Operator) => { - return initialFormValuesMap[operatorName]; - }, - [initialFormValuesMap], - ); - - return initializeOperatorParams; -}; - -export const useHandleDrag = () => { - const handleDragStart = useCallback( - (operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => { - 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<ReactFlowInstance<any, any>>(); - const initializeOperatorParams = useInitializeOperatorParams(); - const getNodeName = useGetNodeName(); - - const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => { - event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; - }, []); - - const onDrop = useCallback( - (event: React.DragEvent<HTMLDivElement>) => { - 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<any> = { - 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<any> = { - 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 }; -}; - -export const useHandleFormValuesChange = (id?: string) => { - const updateNodeForm = useGraphStore((state) => state.updateNodeForm); - const handleValuesChange = useCallback( - (changedValues: any, values: any) => { - let nextValues: any = values; - // Fixed the issue that the related form value does not change after selecting the freedom field of the model - if ( - Object.keys(changedValues).length === 1 && - 'parameter' in changedValues && - changedValues['parameter'] in settledModelVariableMap - ) { - const enabledValues = setChatVariableEnabledFieldValuePage(); - nextValues = { - ...values, - ...settledModelVariableMap[ - changedValues['parameter'] as keyof typeof settledModelVariableMap - ], - ...enabledValues, - }; - } - if (id) { - updateNodeForm(id, nextValues); - } - }, - [updateNodeForm, id], - ); - - return { handleValuesChange }; -}; - -export const useValidateConnection = () => { - const { edges, getOperatorTypeFromId, getParentIdById } = 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], - ); - - // 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 && - !hasLine && - RestrictedUpstreamMap[ - getOperatorTypeFromId(connection.source) as Operator - ]?.every((x) => x !== getOperatorTypeFromId(connection.target)) && - isSameNodeChild(connection); - return ret; - }, - [edges, getOperatorTypeFromId, isSameNodeChild], - ); - - return isValidConnection; -}; - -export const useHandleNodeNameChange = ({ - id, - data, -}: { - id?: string; - data: any; -}) => { - const [name, setName] = useState<string>(''); - const { updateNodeName, nodes } = useGraphStore((state) => state); - const previousName = data?.name; - - 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<any>) => { - setName(e.target.value); - }, []); - - useEffect(() => { - setName(previousName); - }, [previousName]); - - return { name, handleNameBlur, handleNameChange }; -}; - -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, - }; -}; - -/** - * monitor changes in the data.form field of the categorize and relevant operators - * and then synchronize them to the edge - */ -export const useWatchNodeFormDataChange = () => { - const { getNode, nodes, setEdgesByNodeId } = useGraphStore((state) => state); - - const buildCategorizeEdgesByFormData = useCallback( - (nodeId: string, form: ICategorizeForm) => { - // add - // delete - // edit - const categoryDescription = form.category_description; - const downstreamEdges = Object.keys(categoryDescription).reduce<Edge[]>( - (pre, sourceHandle) => { - const target = categoryDescription[sourceHandle]?.to; - if (target) { - pre.push({ - id: uuid(), - source: nodeId, - target, - sourceHandle, - }); - } - - return pre; - }, - [], - ); - - setEdgesByNodeId(nodeId, downstreamEdges); - }, - [setEdgesByNodeId], - ); - - const buildRelevantEdgesByFormData = useCallback( - (nodeId: string, form: IRelevantForm) => { - const downstreamEdges = ['yes', 'no'].reduce<Edge[]>((pre, cur) => { - const target = form[cur as keyof IRelevantForm] as string; - if (target) { - pre.push({ id: uuid(), source: nodeId, target, sourceHandle: cur }); - } - - return pre; - }, []); - - setEdgesByNodeId(nodeId, downstreamEdges); - }, - [setEdgesByNodeId], - ); - - const buildSwitchEdgesByFormData = useCallback( - (nodeId: string, form: ISwitchForm) => { - // add - // delete - // edit - const conditions = form.conditions; - const downstreamEdges = conditions.reduce<Edge[]>((pre, _, idx) => { - const target = conditions[idx]?.to; - if (target) { - pre.push({ - id: uuid(), - source: nodeId, - target, - sourceHandle: generateSwitchHandleText(idx), - }); - } - - return pre; - }, []); - - // Splice the else condition of the conditional judgment to the edge list - const elseTo = form[SwitchElseTo]; - if (elseTo) { - downstreamEdges.push({ - id: uuid(), - source: nodeId, - target: elseTo, - sourceHandle: SwitchElseTo, - }); - } - - setEdgesByNodeId(nodeId, downstreamEdges); - }, - [setEdgesByNodeId], - ); - - useEffect(() => { - nodes.forEach((node) => { - const currentNode = getNode(node.id); - const form = currentNode?.data.form ?? {}; - const operatorType = currentNode?.data.label; - switch (operatorType) { - case Operator.Relevant: - buildRelevantEdgesByFormData(node.id, form as IRelevantForm); - break; - case Operator.Categorize: - buildCategorizeEdgesByFormData(node.id, form as ICategorizeForm); - break; - case Operator.Switch: - buildSwitchEdgesByFormData(node.id, form as ISwitchForm); - break; - default: - break; - } - }); - }, [ - nodes, - buildCategorizeEdgesByFormData, - getNode, - buildRelevantEdgesByFormData, - buildSwitchEdgesByFormData, - ]); -}; - -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/flow/hooks/use-before-delete.tsx b/web/src/pages/flow/hooks/use-before-delete.tsx deleted file mode 100644 index 14512ae96..000000000 --- a/web/src/pages/flow/hooks/use-before-delete.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { OnBeforeDelete } from '@xyflow/react'; -import { Operator } from '../constant'; -import useGraphStore from '../store'; - -const UndeletableNodes = [Operator.Begin, Operator.IterationStart]; - -export function useBeforeDelete() { - const getOperatorTypeFromId = useGraphStore( - (state) => state.getOperatorTypeFromId, - ); - const handleBeforeDelete: OnBeforeDelete<RAGFlowNodeType> = 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; - }); - - return { - nodes: toBeDeletedNodes, - edges: toBeDeletedEdges, - }; - }; - - return { handleBeforeDelete }; -} diff --git a/web/src/pages/flow/hooks/use-build-dsl.ts b/web/src/pages/flow/hooks/use-build-dsl.ts deleted file mode 100644 index 17a0681ed..000000000 --- a/web/src/pages/flow/hooks/use-build-dsl.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { useCallback } from 'react'; -import useGraphStore from '../store'; -import { buildDslComponentsByGraph } from '../utils'; - -export const useBuildDslData = () => { - const { data } = useFetchFlow(); - 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/flow/hooks/use-export-json.ts b/web/src/pages/flow/hooks/use-export-json.ts deleted file mode 100644 index 5b8618de8..000000000 --- a/web/src/pages/flow/hooks/use-export-json.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { FileMimeType } from '@/constants/common'; -import { useSetModalState } from '@/hooks/common-hooks'; -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { IGraph } from '@/interfaces/database/flow'; -import { downloadJsonFile } from '@/utils/file-util'; -import { message, UploadFile } 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 } = useFetchFlow(); - const { t } = useTranslation(); - - const onFileUploadOk = useCallback( - async (fileList: UploadFile[]) => { - if (fileList.length > 0) { - const file: File = fileList[0] as unknown as File; - if (file.type !== FileMimeType.Json) { - message.error(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], - ); - - 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/flow/hooks/use-fetch-data.ts b/web/src/pages/flow/hooks/use-fetch-data.ts deleted file mode 100644 index 245ea6abf..000000000 --- a/web/src/pages/flow/hooks/use-fetch-data.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; -import { IGraph } from '@/interfaces/database/flow'; -import { useEffect } from 'react'; -import { useSetGraphInfo } from './use-set-graph'; - -export const useFetchDataOnMount = () => { - const { loading, data, refetch } = useFetchFlow(); - 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/flow/hooks/use-get-begin-query.tsx b/web/src/pages/flow/hooks/use-get-begin-query.tsx deleted file mode 100644 index 557bbfce0..000000000 --- a/web/src/pages/flow/hooks/use-get-begin-query.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { DefaultOptionType } from 'antd/es/select'; -import get from 'lodash/get'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { BeginId, Operator } from '../constant'; -import { BeginQuery } from '../interface'; -import useGraphStore from '../store'; - -export const useGetBeginNodeDataQuery = () => { - const getNode = useGraphStore((state) => state.getNode); - - const getBeginNodeDataQuery = useCallback(() => { - return get(getNode(BeginId), 'data.form.query', []); - }, [getNode]); - - return getBeginNodeDataQuery; -}; - -export const useGetBeginNodeDataQueryIsSafe = () => { - const [isBeginNodeDataQuerySafe, setIsBeginNodeDataQuerySafe] = - useState(false); - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const nodes = useGraphStore((state) => state.nodes); - - useEffect(() => { - const query: BeginQuery[] = getBeginNodeDataQuery(); - const isSafe = !query.some((q) => !q.optional && q.type === 'file'); - setIsBeginNodeDataQuerySafe(isSafe); - }, [getBeginNodeDataQuery, nodes]); - - return isBeginNodeDataQuerySafe; -}; - -// exclude nodes with branches -const ExcludedNodes = [ - Operator.Categorize, - Operator.Relevant, - Operator.Begin, - Operator.Note, -]; - -export const useBuildComponentIdSelectOptions = ( - nodeId?: string, - parentId?: string, -) => { - const nodes = useGraphStore((state) => state.nodes); - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const query: BeginQuery[] = getBeginNodeDataQuery(); - - // 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]); - - const groupedOptions = [ - { - label: <span>Component Output</span>, - title: 'Component Output', - options: componentIdOptions, - }, - { - label: <span>Begin Input</span>, - title: 'Begin Input', - options: query.map((x) => ({ - label: x.name, - value: `begin@${x.key}`, - })), - }, - ]; - - return groupedOptions; -}; - -export const useGetComponentLabelByValue = (nodeId: string) => { - const options = useBuildComponentIdSelectOptions(nodeId); - const flattenOptions = useMemo( - () => - options.reduce<DefaultOptionType[]>((pre, cur) => { - return [...pre, ...cur.options]; - }, []), - [options], - ); - - const getLabel = useCallback( - (val?: string) => { - return flattenOptions.find((x) => x.value === val)?.label; - }, - [flattenOptions], - ); - return getLabel; -}; diff --git a/web/src/pages/flow/hooks/use-iteration.ts b/web/src/pages/flow/hooks/use-iteration.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/src/pages/flow/hooks/use-open-document.ts b/web/src/pages/flow/hooks/use-open-document.ts deleted file mode 100644 index 384529c15..000000000 --- a/web/src/pages/flow/hooks/use-open-document.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/flow/hooks/use-save-graph.ts b/web/src/pages/flow/hooks/use-save-graph.ts deleted file mode 100644 index 89507bdae..000000000 --- a/web/src/pages/flow/hooks/use-save-graph.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useFetchFlow, useResetFlow, useSetFlow } from '@/hooks/flow-hooks'; -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { useDebounceEffect } from 'ahooks'; -import dayjs from 'dayjs'; -import { useCallback, useEffect, useState } from 'react'; -import { useParams } from 'umi'; -import useGraphStore from '../store'; -import { useBuildDslData } from './use-build-dsl'; - -export const useSaveGraph = () => { - const { data } = useFetchFlow(); - const { setFlow, loading } = useSetFlow(); - const { id } = useParams(); - const { buildDslData } = useBuildDslData(); - - const saveGraph = useCallback( - async (currentNodes?: RAGFlowNodeType[]) => { - return setFlow({ - id, - title: data.title, - dsl: buildDslData(currentNodes), - }); - }, - [setFlow, id, data.title, buildDslData], - ); - - return { saveGraph, loading }; -}; - -export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { - const { saveGraph, loading } = useSaveGraph(); - const { resetFlow } = useResetFlow(); - - 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 resetFlow(); - // After resetting, all previous messages will be cleared. - if (resetRet?.code === 0) { - show(); - } - } - }, - [saveGraph, resetFlow, show], - ); - - return { handleRun, loading }; -}; - -export const useWatchAgentChange = (chatDrawerVisible: boolean) => { - console.log( - '🚀 ~ useWatchAgentChange ~ chatDrawerVisible:', - chatDrawerVisible, - ); - const [time, setTime] = useState<string>(); - const nodes = useGraphStore((state) => state.nodes); - const edges = useGraphStore((state) => state.edges); - // const { saveGraph } = useSaveGraph(); - const { data: flowDetail } = useFetchFlow(); - - const setSaveTime = useCallback((updateTime: number) => { - setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')); - }, []); - - 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/flow/hooks/use-set-graph.ts b/web/src/pages/flow/hooks/use-set-graph.ts deleted file mode 100644 index 6dd68a330..000000000 --- a/web/src/pages/flow/hooks/use-set-graph.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/flow/hooks/use-show-drawer.tsx b/web/src/pages/flow/hooks/use-show-drawer.tsx deleted file mode 100644 index efc4cf32a..000000000 --- a/web/src/pages/flow/hooks/use-show-drawer.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useSetModalState } from '@/hooks/common-hooks'; -import { Node, NodeMouseHandler } from '@xyflow/react'; -import get from 'lodash/get'; -import { useCallback, useEffect } from 'react'; -import { Operator } from '../constant'; -import { BeginQuery } from '../interface'; -import useGraphStore from '../store'; -import { useGetBeginNodeDataQuery } from './use-get-begin-query'; -import { useSaveGraph } from './use-save-graph'; - -export const useShowFormDrawer = () => { - const { - clickedNodeId: clickNodeId, - setClickedNodeId, - getNode, - } = useGraphStore((state) => state); - const { - visible: formDrawerVisible, - hideModal: hideFormDrawer, - showModal: showFormDrawer, - } = useSetModalState(); - - const handleShow = useCallback( - (node: Node) => { - setClickedNodeId(node.id); - showFormDrawer(); - }, - [showFormDrawer, setClickedNodeId], - ); - - 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.IterationStart, 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 getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - - useEffect(() => { - if (drawerVisible) { - const query: BeginQuery[] = getBeginNodeDataQuery(); - if (query.length > 0) { - showRunModal(); - hideChatModal(); - } else { - showChatModal(); - hideRunModal(); - } - } - }, [ - hideChatModal, - hideRunModal, - showChatModal, - showRunModal, - drawerVisible, - getBeginNodeDataQuery, - ]); - - 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(node); - } - // handle single debug icon click - if ( - get(e.target, 'dataset.play') === 'true' || - get(e.target, 'parentNode.dataset.play') === 'true' - ) { - showSingleDebugDrawer(); - } - }, - [ - hideRunOrChatDrawer, - hideSingleDebugDrawer, - showFormDrawer, - showSingleDebugDrawer, - ], - ); - - return { - chatVisible, - runVisible, - onPaneClick, - singleDebugDrawerVisible, - showSingleDebugDrawer, - hideSingleDebugDrawer, - formDrawerVisible, - showFormDrawer, - clickedNode, - onNodeClick, - hideFormDrawer, - hideRunOrChatDrawer, - showChatModal, - }; -} diff --git a/web/src/pages/flow/index.tsx b/web/src/pages/flow/index.tsx deleted file mode 100644 index 1d758e347..000000000 --- a/web/src/pages/flow/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useSetModalState } from '@/hooks/common-hooks'; -import { ReactFlowProvider } from '@xyflow/react'; -import { Layout } from 'antd'; -import { useState } from 'react'; -import FlowCanvas from './canvas'; -import Sider from './flow-sider'; -import FlowHeader from './header'; -import { useCopyPaste } from './hooks'; -import { useFetchDataOnMount } from './hooks/use-fetch-data'; - -const { Content } = Layout; - -function RagFlow() { - const [collapsed, setCollapsed] = useState(false); - const { - visible: chatDrawerVisible, - hideModal: hideChatDrawer, - showModal: showChatDrawer, - } = useSetModalState(); - - useFetchDataOnMount(); - useCopyPaste(); - - return ( - <Layout> - <ReactFlowProvider> - <Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider> - <Layout> - <FlowHeader - showChatDrawer={showChatDrawer} - chatDrawerVisible={chatDrawerVisible} - ></FlowHeader> - <Content style={{ margin: 0 }}> - <FlowCanvas - drawerVisible={chatDrawerVisible} - hideDrawer={hideChatDrawer} - ></FlowCanvas> - </Content> - </Layout> - </ReactFlowProvider> - </Layout> - ); -} - -export default RagFlow; diff --git a/web/src/pages/flow/interface.ts b/web/src/pages/flow/interface.ts deleted file mode 100644 index ff70f1e69..000000000 --- a/web/src/pages/flow/interface.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 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)[]; -} diff --git a/web/src/pages/flow/interpreter.json b/web/src/pages/flow/interpreter.json deleted file mode 100644 index 8bc5f149d..000000000 --- a/web/src/pages/flow/interpreter.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "edges": [ - { - "id": "c87c7805-8cf0-4cd4-b45b-152031811020", - "label": "", - "source": "begin", - "target": "answer:0" - }, - { - "id": "e30320bb-601b-4885-acb3-79becdc49f08", - "label": "", - "source": "generate:0", - "target": "answer:0" - }, - { - "id": "83927e42-739a-402a-9f75-a88d4fab37ed", - "label": "", - "source": "answer:0", - "target": "generate:0" - } - ], - "nodes": [ - { - "id": "begin", - "type": "beginNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Begin", - "name": "FruityPianosSend", - "form": { - "prologue": "Hi there! Please enter the text you want to translate in format like: 'text you want to translate' => target language. For an example: 您好! => English" - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "answer:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Answer", - "name": "YummyBoatsFlow", - "form": {} - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "SwiftTramsDrop", - "form": { - "llm_id": "deepseek-chat", - "prompt": "You are an professional interpreter.\n- Role: an professional interpreter.\n- Input format: content need to be translated => target language. \n- Answer format: => translated content in target language. \n- Examples:\n - user: 您好! => English. assistant: => How are you doing!\n - user: You look good today. => Japanese. assistant: => 今日は調子がいいですね 。\n", - "temperature": 0.5 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - } - ] -} diff --git a/web/src/pages/flow/json-upload-modal/index.less b/web/src/pages/flow/json-upload-modal/index.less deleted file mode 100644 index 8472339fe..000000000 --- a/web/src/pages/flow/json-upload-modal/index.less +++ /dev/null @@ -1,13 +0,0 @@ -.uploader { - :global { - .ant-upload-list { - max-height: 40vh; - overflow-y: auto; - } - } -} - -.uploadLimit { - color: red; - font-size: 12px; -} diff --git a/web/src/pages/flow/json-upload-modal/index.tsx b/web/src/pages/flow/json-upload-modal/index.tsx deleted file mode 100644 index 085ecf349..000000000 --- a/web/src/pages/flow/json-upload-modal/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { InboxOutlined } from '@ant-design/icons'; -import { Modal, Upload, UploadFile, UploadProps } from 'antd'; -import { Dispatch, SetStateAction, useState } from 'react'; - -import { FileMimeType } from '@/constants/common'; - -import styles from './index.less'; - -const { Dragger } = Upload; - -const FileUpload = ({ - directory, - fileList, - setFileList, -}: { - directory: boolean; - fileList: UploadFile[]; - setFileList: Dispatch<SetStateAction<UploadFile[]>>; -}) => { - const { t } = useTranslate('fileManager'); - const props: UploadProps = { - multiple: false, - accept: FileMimeType.Json, - onRemove: (file) => { - const index = fileList.indexOf(file); - const newFileList = fileList.slice(); - newFileList.splice(index, 1); - setFileList(newFileList); - }, - beforeUpload: (file) => { - setFileList(() => { - return [file]; - }); - - return false; - }, - directory, - fileList, - }; - - return ( - <Dragger {...props} className={styles.uploader}> - <p className="ant-upload-drag-icon"> - <InboxOutlined /> - </p> - <p className="ant-upload-text">{t('uploadTitle')}</p> - <p className="ant-upload-hint">{t('uploadDescription')}</p> - {false && <p className={styles.uploadLimit}>{t('uploadLimit')}</p>} - </Dragger> - ); -}; - -const JsonUploadModal = ({ - visible, - hideModal, - loading, - onOk: onFileUploadOk, -}: IModalProps<UploadFile[]>) => { - const { t } = useTranslate('fileManager'); - const [fileList, setFileList] = useState<UploadFile[]>([]); - const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]); - - const clearFileList = () => { - setFileList([]); - setDirectoryFileList([]); - }; - - const onOk = async () => { - const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]); - return ret; - }; - - const afterClose = () => { - clearFileList(); - }; - - return ( - <Modal - title={t('uploadFile')} - open={visible} - onOk={onOk} - onCancel={hideModal} - confirmLoading={loading} - afterClose={afterClose} - > - <FileUpload - directory={false} - fileList={fileList} - setFileList={setFileList} - ></FileUpload> - </Modal> - ); -}; - -export default JsonUploadModal; diff --git a/web/src/pages/flow/list/agent-template-modal.tsx b/web/src/pages/flow/list/agent-template-modal.tsx deleted file mode 100644 index ac19ff0de..000000000 --- a/web/src/pages/flow/list/agent-template-modal.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { IModalManagerChildrenProps } from '@/components/modal-manager'; -import { useSetModalState, useTranslate } from '@/hooks/common-hooks'; -import { IFlowTemplate } from '@/interfaces/database/flow'; -// import { useFetchFlowTemplates } from '@/hooks/flow-hooks'; -import { useSelectItem } from '@/hooks/logic-hooks'; -import { Button, Card, Flex, List, Modal, Typography } from 'antd'; -import { useCallback, useState } from 'react'; -import CreateAgentModal from './create-agent-modal'; -import GraphAvatar from './graph-avatar'; - -import DOMPurify from 'dompurify'; -import styles from './index.less'; - -const { Title, Text, Paragraph } = Typography; -interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> { - loading: boolean; - onOk: (name: string, templateId: string) => void; - showModal?(): void; - templateList: IFlowTemplate[]; -} - -const AgentTemplateModal = ({ - visible, - hideModal, - loading, - onOk, - templateList, -}: IProps) => { - const { t } = useTranslate('common'); - // const { data: list } = useFetchFlowTemplates(); - const { selectedId, handleItemClick } = useSelectItem(''); - const [checkedId, setCheckedId] = useState<string>(''); - - const { - visible: creatingVisible, - hideModal: hideCreatingModal, - showModal: showCreatingModal, - } = useSetModalState(); - - const handleOk = useCallback( - async (name: string) => { - return onOk(name, checkedId); - }, - [onOk, checkedId], - ); - - const onShowCreatingModal = useCallback( - (id: string) => () => { - showCreatingModal(); - setCheckedId(id); - }, - [showCreatingModal], - ); - - return ( - <Modal - title={t('createGraph', { keyPrefix: 'flow' })} - open={visible} - width={'100vw'} - onCancel={hideModal} - okButtonProps={{ loading }} - confirmLoading={loading} - className={styles.agentTemplateModal} - wrapClassName={styles.agentTemplateModalWrapper} - footer={null} - > - <section className={styles.createModalContent}> - <Title level={5}> - {t('createFromTemplates', { keyPrefix: 'flow' })} - - ( - - - - - - - {x.title} - - - -
- -
-
-
- {selectedId === x.id && ( - - )} -
-
- )} - /> - - {creatingVisible && ( - - )} - - ); -}; - -export default AgentTemplateModal; diff --git a/web/src/pages/flow/list/create-agent-modal.tsx b/web/src/pages/flow/list/create-agent-modal.tsx deleted file mode 100644 index 52e6601c8..000000000 --- a/web/src/pages/flow/list/create-agent-modal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { IModalManagerChildrenProps } from '@/components/modal-manager'; -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Modal } from 'antd'; - -interface IProps extends Omit { - loading: boolean; - onOk: (name: string) => void; - showModal?(): void; -} - -const CreateAgentModal = ({ visible, hideModal, loading, onOk }: IProps) => { - const [form] = Form.useForm(); - const { t } = useTranslate('common'); - - type FieldType = { - name?: string; - }; - - const handleOk = async () => { - const ret = await form.validateFields(); - - return onOk(ret.name); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - handleOk(); - } - }; - - return ( - -
- - label={t('name')} - name="name" - rules={[{ required: true, message: t('namePlaceholder') }]} - > - - - -
- ); -}; - -export default CreateAgentModal; diff --git a/web/src/pages/flow/list/flow-card/index.less b/web/src/pages/flow/list/flow-card/index.less deleted file mode 100644 index d483d46f0..000000000 --- a/web/src/pages/flow/list/flow-card/index.less +++ /dev/null @@ -1,84 +0,0 @@ -.container { - height: 160px; - display: flex; - flex-direction: column; - justify-content: space-between; - - .delete { - height: 24px; - } - - .content { - display: flex; - justify-content: space-between; - - .context { - flex: 1; - } - } - - .footer { - // text-align: left; - } - .footerTop { - padding-bottom: 2px; - } -} - -.card { - border-radius: 12px; - border: 1px solid rgba(0, 0, 0, 0.3); - background-color: rgba(255, 255, 255, 0.1); - box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); - padding: 24px; - width: 300px; - cursor: pointer; - - .titleWrapper { - // flex: 1; - .title { - font-size: 24px; - line-height: 32px; - font-weight: 600; - word-break: break-all; - } - .description { - font-size: 12px; - font-weight: 600; - line-height: 20px; - } - } - - :global { - .ant-card-body { - padding: 0; - margin: 0; - } - } - .bottom { - display: flex; - align-items: center; - justify-content: space-between; - } - .bottomLeft { - vertical-align: middle; - } - .leftIcon { - margin-right: 10px; - font-size: 18px; - vertical-align: middle; - } - .rightText { - font-size: 12px; - font-weight: 600; - vertical-align: middle; - } -} - -.hideRibbon { - display: none !important; -} - -.ribbon { - top: 4px; -} diff --git a/web/src/pages/flow/list/flow-card/index.tsx b/web/src/pages/flow/list/flow-card/index.tsx deleted file mode 100644 index 38586a8e7..000000000 --- a/web/src/pages/flow/list/flow-card/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { formatDate } from '@/utils/date'; -import { CalendarOutlined } from '@ant-design/icons'; -import { Badge, Card, Typography } from 'antd'; -import { useNavigate } from 'umi'; - -import OperateDropdown from '@/components/operate-dropdown'; -import { useDeleteFlow } from '@/hooks/flow-hooks'; -import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; -import { IFlow } from '@/interfaces/database/flow'; -import classNames from 'classnames'; -import { useCallback } from 'react'; -import GraphAvatar from '../graph-avatar'; -import styles from './index.less'; - -interface IProps { - item: IFlow; - onDelete?: (string: string) => void; -} - -const FlowCard = ({ item }: IProps) => { - const navigate = useNavigate(); - const { deleteFlow } = useDeleteFlow(); - const { data: userInfo } = useFetchUserInfo(); - - const removeFlow = useCallback(() => { - return deleteFlow([item.id]); - }, [deleteFlow, item]); - - const handleCardClick = () => { - navigate(`/flow/${item.id}`); - }; - - return ( - - -
-
- - -
-
- - {item.title} - -

- {item.description} -

-
-
-
-
- - - {formatDate(item.update_time)} - -
-
-
-
-
-
- ); -}; - -export default FlowCard; diff --git a/web/src/pages/flow/list/graph-avatar.tsx b/web/src/pages/flow/list/graph-avatar.tsx deleted file mode 100644 index 5d1dedecb..000000000 --- a/web/src/pages/flow/list/graph-avatar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { ReactComponent as NothingIcon } from '@/assets/svg/nothing.svg'; -import { Avatar } from 'antd'; - -const GraphAvatar = ({ avatar }: { avatar?: string | null }) => { - return ( -
- {avatar ? ( - } src={avatar} /> - ) : ( - - )} -
- ); -}; - -export default GraphAvatar; diff --git a/web/src/pages/flow/list/hooks.ts b/web/src/pages/flow/list/hooks.ts deleted file mode 100644 index 35c994d56..000000000 --- a/web/src/pages/flow/list/hooks.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useSetModalState } from '@/hooks/common-hooks'; -import { useFetchFlowTemplates, useSetFlow } from '@/hooks/flow-hooks'; -import { useHandleSearchChange } from '@/hooks/logic-hooks'; -import flowService from '@/services/flow-service'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { useDebounce } from 'ahooks'; -import { useCallback } from 'react'; -import { useNavigate } from 'umi'; - -export const useFetchDataOnMount = () => { - const { searchString, handleInputChange } = useHandleSearchChange(); - const debouncedSearchString = useDebounce(searchString, { wait: 500 }); - - const PageSize = 30; - const { - data, - error, - fetchNextPage, - hasNextPage, - isFetching, - isFetchingNextPage, - status, - } = useInfiniteQuery({ - queryKey: ['infiniteFetchFlowListTeam', debouncedSearchString], - queryFn: async ({ pageParam }) => { - const { data } = await flowService.listCanvasTeam({ - page: pageParam, - page_size: PageSize, - keywords: debouncedSearchString, - }); - const list = data?.data ?? []; - return list; - }, - initialPageParam: 1, - getNextPageParam: (lastPage, pages, lastPageParam) => { - if (lastPageParam * PageSize <= lastPage.total) { - return lastPageParam + 1; - } - return undefined; - }, - }); - return { - data, - loading: isFetching, - error, - fetchNextPage, - hasNextPage, - isFetching, - isFetchingNextPage, - status, - handleInputChange, - searchString, - }; -}; - -export const useSaveFlow = () => { - const { - visible: flowSettingVisible, - hideModal: hideFlowSettingModal, - showModal: showFileRenameModal, - } = useSetModalState(); - const { loading, setFlow } = useSetFlow(); - const navigate = useNavigate(); - const { data: list } = useFetchFlowTemplates(); - - const onFlowOk = useCallback( - async (title: string, templateId: string) => { - const templateItem = list.find((x) => x.id === templateId); - - let dsl = templateItem?.dsl; - const ret = await setFlow({ - title, - dsl, - avatar: templateItem?.avatar, - }); - - if (ret?.code === 0) { - hideFlowSettingModal(); - navigate(`/flow/${ret.data.id}`); - } - }, - [setFlow, hideFlowSettingModal, navigate, list], - ); - - return { - flowSettingLoading: loading, - initialFlowName: '', - onFlowOk, - flowSettingVisible, - hideFlowSettingModal, - templateList: list, - showFlowSettingModal: showFileRenameModal, - }; -}; diff --git a/web/src/pages/flow/list/index.less b/web/src/pages/flow/list/index.less deleted file mode 100644 index 7dffd8a32..000000000 --- a/web/src/pages/flow/list/index.less +++ /dev/null @@ -1,99 +0,0 @@ -.flowListWrapper { - padding: 48px; -} - -.topWrapper { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding: 0 60px 72px; - - .title { - font-family: Inter; - font-size: 30px; - font-style: normal; - font-weight: @fontWeight600; - line-height: 38px; - color: rgba(16, 24, 40, 1); - } - .description { - font-family: Inter; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: 24px; - color: rgba(71, 84, 103, 1); - } - - .topButton { - font-family: Inter; - font-size: 14px; - font-style: normal; - font-weight: @fontWeight600; - line-height: 20px; - } - - .filterButton { - display: flex; - align-items: center; - .topButton(); - } -} -.flowCardContainer { - padding: 0 60px; - overflow: auto; - .knowledgeEmpty { - width: 100%; - } -} - -.templatesBox { - max-height: 70vh; - overflow: auto; -} - -.agentTemplateModal { - top: 0; - margin: 0; - width: 100%; - max-width: 100%; - height: 100vh; - max-height: 100vh; - padding: 0; - - :global(.ant-modal-content) { - // width: 100vw; - height: 100%; - border-radius: 0; - } - .agentDescription { - padding-top: 18px; - height: 110px; - } - .createModalContent { - height: 90vh; - } - .agentTitleWrapper { - width: 80%; - } - .flowTemplateCard { - position: relative; - cursor: pointer; - } - - .selectedFlowTemplateCard { - background-color: @selectedBackgroundColor; - } - .useButton { - position: absolute; - width: 84%; - left: 0; - right: 0; - bottom: 10px; - margin: auto; - } -} - -.agentTemplateModalWrapper { - margin: 0; -} diff --git a/web/src/pages/flow/list/index.tsx b/web/src/pages/flow/list/index.tsx deleted file mode 100644 index 9bc0a845d..000000000 --- a/web/src/pages/flow/list/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; -import { - Button, - Divider, - Empty, - Flex, - Input, - Skeleton, - Space, - Spin, -} from 'antd'; -import AgentTemplateModal from './agent-template-modal'; -import FlowCard from './flow-card'; -import { useFetchDataOnMount, useSaveFlow } from './hooks'; - -import { useTranslate } from '@/hooks/common-hooks'; -import { useMemo } from 'react'; -import InfiniteScroll from 'react-infinite-scroll-component'; -import styles from './index.less'; - -const FlowList = () => { - const { - showFlowSettingModal, - hideFlowSettingModal, - flowSettingVisible, - flowSettingLoading, - templateList, - onFlowOk, - } = useSaveFlow(); - const { t } = useTranslate('flow'); - - const { - data, - loading, - searchString, - handleInputChange, - fetchNextPage, - hasNextPage, - } = useFetchDataOnMount(); - - const nextList = useMemo(() => { - const list = - data?.pages?.flatMap((x) => (Array.isArray(x.kbs) ? x.kbs : [])) ?? []; - return list; - }, [data?.pages]); - - const total = useMemo(() => { - return data?.pages.at(-1).total ?? 0; - }, [data?.pages]); - - return ( - - - - } - /> - - - - - -
- } - endMessage={ - !!total && {t('noMoreData')} 🤐 - } - scrollableTarget="scrollableDiv" - scrollThreshold="200px" - > - - {nextList.length > 0 ? ( - nextList.map((item) => { - return ; - }) - ) : ( - - )} - - -
-
- {flowSettingVisible && ( - - )} -
- ); -}; - -export default FlowList; diff --git a/web/src/pages/flow/mock.tsx b/web/src/pages/flow/mock.tsx deleted file mode 100644 index 79c61d73d..000000000 --- a/web/src/pages/flow/mock.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { Position } from '@xyflow/react'; - -export const initialNodes = [ - { - sourcePosition: Position.Left, - targetPosition: Position.Right, - id: 'node-1', - type: 'ragNode', - position: { x: 0, y: 0 }, - // position: { x: 400, y: 100 }, - data: { label: 123 }, - }, - { - sourcePosition: Position.Right, - targetPosition: Position.Left, - id: '1', - data: { label: 'Hello' }, - position: { x: 0, y: 0 }, - // position: { x: 0, y: 50 }, - type: 'input', - }, - { - sourcePosition: Position.Right, - targetPosition: Position.Left, - id: '2', - data: { label: 'World' }, - position: { x: 0, y: 0 }, - // position: { x: 200, y: 50 }, - }, -]; - -export const initialEdges = [ - { id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' }, -]; - -export const dsl = { - graph: { - nodes: [ - { - id: 'Begin', - type: 'beginNode', - position: { - x: 50, - y: 200, - }, - data: { - label: 'Begin', - name: 'begin', - }, - sourcePosition: 'left', - targetPosition: 'right', - }, - // { - // id: 'Answer:China', - // type: 'ragNode', - // position: { - // x: 150, - // y: 200, - // }, - // data: { - // label: 'Answer', - // }, - // sourcePosition: 'left', - // targetPosition: 'right', - // }, - // { - // id: 'Retrieval:China', - // type: 'ragNode', - // position: { - // x: 250, - // y: 200, - // }, - // data: { - // label: 'Retrieval', - // }, - // sourcePosition: 'left', - // targetPosition: 'right', - // }, - // { - // id: 'Generate:China', - // type: 'ragNode', - // position: { - // x: 100, - // y: 100, - // }, - // data: { - // label: 'Generate', - // }, - // sourcePosition: 'left', - // targetPosition: 'right', - // }, - ], - edges: [ - // { - // id: '7facb53d-65c9-43b3-ac55-339c445d3891', - // label: '', - // source: 'begin', - // target: 'Answer:China', - // markerEnd: { - // type: 'arrow', - // }, - // }, - // { - // id: '7ac83631-502d-410f-a6e7-bec6866a5e99', - // label: '', - // source: 'Generate:China', - // target: 'Answer:China', - // markerEnd: { - // type: 'arrow', - // }, - // }, - // { - // id: '0aaab297-5779-43ed-9281-2c4d3741566f', - // label: '', - // source: 'Answer:China', - // target: 'Retrieval:China', - // markerEnd: { - // type: 'arrow', - // }, - // }, - // { - // id: '3477f9f3-0a7d-400e-af96-a11ea7673183', - // label: '', - // source: 'Retrieval:China', - // target: 'Generate:China', - // markerEnd: { - // type: 'arrow', - // }, - // }, - ], - }, - components: { - begin: { - obj: { - component_name: 'Begin', - params: {}, - }, - downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id - upstream: [], // edge source is upstream, edge target is current node id - }, - // 'Answer:China': { - // obj: { - // component_name: 'Answer', - // params: {}, - // }, - // downstream: ['Retrieval:China'], - // upstream: ['begin', 'Generate:China'], - // }, - // 'Retrieval:China': { - // obj: { - // component_name: 'Retrieval', - // params: { - // similarity_threshold: 0.2, - // keywords_similarity_weight: 0.3, - // top_n: 6, - // top_k: 1024, - // rerank_id: 'BAAI/bge-reranker-v2-m3', - // kb_ids: ['568aa82603b611efa9d9fa163e197198'], - // }, - // }, - // downstream: ['Generate:China'], - // upstream: ['Answer:China'], - // }, - // 'Generate:China': { - // obj: { - // component_name: 'Generate', - // params: { - // llm_id: 'deepseek-chat', - // prompt: - // 'You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence "The answer you are looking for is not found in the knowledge base!" Answers need to consider chat history.\n Here is the knowledge base:\n {input}\n The above is the knowledge base.', - // temperature: 0.2, - // }, - // }, - // downstream: ['Answer:China'], - // upstream: ['Retrieval:China'], - // }, - }, - messages: [], - reference: [], - history: [], - path: [], - answer: [], -}; diff --git a/web/src/pages/flow/operator-icon/index.less b/web/src/pages/flow/operator-icon/index.less deleted file mode 100644 index a8746c13b..000000000 --- a/web/src/pages/flow/operator-icon/index.less +++ /dev/null @@ -1,6 +0,0 @@ -.icon { - color: rgb(59, 118, 244); - font-size: 24px; - max-width: 24px; - max-height: 24px; -} diff --git a/web/src/pages/flow/operator-icon/index.tsx b/web/src/pages/flow/operator-icon/index.tsx deleted file mode 100644 index c6f74b5f6..000000000 --- a/web/src/pages/flow/operator-icon/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { Operator, operatorIconMap } from '../constant'; - -import styles from './index.less'; - -interface IProps { - name: Operator; - fontSize?: number; - width?: number; - color?: string; -} - -const OperatorIcon = ({ name, fontSize, width, color }: IProps) => { - const Icon = operatorIconMap[name] || React.Fragment; - return ( - - ); -}; - -export default OperatorIcon; diff --git a/web/src/pages/flow/retrieval_relevant_rewrite_and_generate.json b/web/src/pages/flow/retrieval_relevant_rewrite_and_generate.json deleted file mode 100644 index 5c14b16a1..000000000 --- a/web/src/pages/flow/retrieval_relevant_rewrite_and_generate.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "edges": [ - { - "id": "81de838d-a541-4b3f-9d68-9172ffd7c6b4", - "label": "", - "source": "begin", - "target": "answer:0" - }, - { - "id": "8fa8daaa-58e7-4494-84c9-d53f379d2550", - "label": "", - "source": "generate:0", - "target": "answer:0" - }, - { - "id": "6720a5b8-96bc-4535-8800-ad3f35431a16", - "label": "", - "source": "answer:0", - "target": "retrieval:0" - }, - { - "id": "81476d89-707f-4d87-8aa3-fecb9d8499b3", - "label": "", - "source": "rewrite:0", - "target": "retrieval:0" - }, - { - "id": "2bba4a81-44e7-4796-b25f-e68c3fc7e54a", - "label": "", - "source": "retrieval:0", - "target": "relevant:0" - }, - { - "id": "eb205b7a-a87e-4bcc-94c5-bddff13f8ddd", - "label": "", - "source": "relevant:0", - "target": "generate:0" - }, - { - "id": "b8611b17-a01a-485c-ad40-377329eb8d96", - "label": "", - "source": "relevant:0", - "target": "rewrite:0" - } - ], - "nodes": [ - { - "id": "begin", - "type": "beginNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Begin", - "name": "FiftyDeerDeny", - "form": { - "prologue": "Hi there!" - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "answer:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Answer", - "name": "NinePointsSmoke", - "form": {} - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "retrieval:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Retrieval", - "name": "ProudLiesPull", - "form": { - "similarity_threshold": 0.2, - "keywords_similarity_weight": 0.3, - "top_n": 6, - "top_k": 1024, - "rerank_id": "BAAI/bge-reranker-v2-m3", - "kb_ids": ["869a236818b811ef91dffa163e197198"], - "empty_response": "Sorry, knowledge base has noting related information." - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "relevant:0", - "type": "relevantNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Relevant", - "name": "StrongBooksPay", - "form": { - "llm_id": "deepseek-chat", - "temperature": 0.02, - "yes": "generate:0", - "no": "rewrite:0" - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "generate:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "Generate", - "name": "CyanBooksTell", - "form": { - "llm_id": "deepseek-chat", - "prompt": "You are an intelligent assistant. Please answer the question based on content of knowledge base. When all knowledge base content is irrelevant to the question, your answer must include the sentence \"The answer you are looking for is not found in the knowledge base!\". Answers need to consider chat history.\n Knowledge base content is as following:\n {input}\n The above is the content of knowledge base.", - "temperature": 0.02 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - }, - { - "id": "rewrite:0", - "type": "ragNode", - "position": { - "x": 0, - "y": 0 - }, - "data": { - "label": "RewriteQuestion", - "name": "SourPapersMake", - "form": { - "llm_id": "deepseek-chat", - "temperature": 0.8 - } - }, - "sourcePosition": "left", - "targetPosition": "right" - } - ] -} diff --git a/web/src/pages/flow/run-drawer/index.less b/web/src/pages/flow/run-drawer/index.less deleted file mode 100644 index fda707810..000000000 --- a/web/src/pages/flow/run-drawer/index.less +++ /dev/null @@ -1,5 +0,0 @@ -.formWrapper { - :global(.ant-form-item-label) { - font-weight: 600 !important; - } -} diff --git a/web/src/pages/flow/run-drawer/index.tsx b/web/src/pages/flow/run-drawer/index.tsx deleted file mode 100644 index d6cf4593a..000000000 --- a/web/src/pages/flow/run-drawer/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { IModalProps } from '@/interfaces/common'; -import { Drawer } from 'antd'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BeginId } from '../constant'; -import DebugContent from '../debug-content'; -import { useGetBeginNodeDataQuery } from '../hooks/use-get-begin-query'; -import { useSaveGraphBeforeOpeningDebugDrawer } from '../hooks/use-save-graph'; -import { BeginQuery } from '../interface'; -import useGraphStore from '../store'; -import { getDrawerWidth } from '../utils'; - -const RunDrawer = ({ - hideModal, - showModal: showChatModal, -}: IModalProps) => { - const { t } = useTranslation(); - const updateNodeForm = useGraphStore((state) => state.updateNodeForm); - - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const query: BeginQuery[] = getBeginNodeDataQuery(); - - const { handleRun, loading } = useSaveGraphBeforeOpeningDebugDrawer( - showChatModal!, - ); - - const handleRunAgent = useCallback( - (nextValues: Record) => { - const currentNodes = updateNodeForm(BeginId, nextValues, ['query']); - handleRun(currentNodes); - hideModal?.(); - }, - [handleRun, hideModal, updateNodeForm], - ); - - const onOk = useCallback( - async (nextValues: any[]) => { - handleRunAgent(nextValues); - }, - [handleRunAgent], - ); - - return ( - - - - ); -}; - -export default RunDrawer; diff --git a/web/src/pages/flow/run-drawer/popover-form.tsx b/web/src/pages/flow/run-drawer/popover-form.tsx deleted file mode 100644 index 557e3185b..000000000 --- a/web/src/pages/flow/run-drawer/popover-form.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useParseDocument } from '@/hooks/document-hooks'; -import { useResetFormOnCloseModal } from '@/hooks/logic-hooks'; -import { IModalProps } from '@/interfaces/common'; -import { Button, Form, Input, Popover } from 'antd'; -import { PropsWithChildren } from 'react'; -import { useTranslation } from 'react-i18next'; - -const reg = - /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/; - -export const PopoverForm = ({ - children, - visible, - switchVisible, -}: PropsWithChildren>) => { - const [form] = Form.useForm(); - const { parseDocument, loading } = useParseDocument(); - const { t } = useTranslation(); - - useResetFormOnCloseModal({ - form, - visible, - }); - - const onOk = async () => { - const values = await form.validateFields(); - const val = values.url; - - if (reg.test(val)) { - const ret = await parseDocument(val); - if (ret?.data?.code === 0) { - form.setFieldValue('result', ret?.data?.data); - form.submit(); - } - } - }; - - const content = ( -
- - e.preventDefault()} - placeholder={t('flow.pasteFileLink')} - suffix={ - - } - /> - - - - ); - - return ( - - {children} - - ); -}; diff --git a/web/src/pages/flow/store.ts b/web/src/pages/flow/store.ts deleted file mode 100644 index c6e8c69b6..000000000 --- a/web/src/pages/flow/store.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import type {} from '@redux-devtools/extension'; -import { - Connection, - Edge, - EdgeChange, - OnConnect, - OnEdgesChange, - OnNodesChange, - OnSelectionChangeFunc, - OnSelectionChangeParams, - addEdge, - applyEdgeChanges, - applyNodeChanges, -} from '@xyflow/react'; -import { omit } from 'lodash'; -import differenceWith from 'lodash/differenceWith'; -import intersectionWith from 'lodash/intersectionWith'; -import lodashSet from 'lodash/set'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; -import { Operator, SwitchElseTo } from './constant'; -import { - duplicateNodeForm, - generateDuplicateNode, - generateNodeNamesWithIncreasingIndex, - getOperatorIndex, - isEdgeEqual, -} from './utils'; - -export type RFState = { - nodes: RAGFlowNodeType[]; - edges: Edge[]; - selectedNodeIds: string[]; - selectedEdgeIds: string[]; - clickedNodeId: string; // currently selected node - onNodesChange: OnNodesChange; - onEdgesChange: OnEdgesChange; - onConnect: OnConnect; - setNodes: (nodes: RAGFlowNodeType[]) => void; - setEdges: (edges: Edge[]) => void; - setEdgesByNodeId: (nodeId: string, edges: Edge[]) => void; - updateNodeForm: ( - nodeId: string, - values: any, - path?: (string | number)[], - ) => RAGFlowNodeType[]; - onSelectionChange: OnSelectionChangeFunc; - addNode: (nodes: RAGFlowNodeType) => void; - getNode: (id?: string | null) => RAGFlowNodeType | undefined; - addEdge: (connection: Connection) => void; - getEdge: (id: string) => Edge | undefined; - updateFormDataOnConnect: (connection: Connection) => void; - updateSwitchFormData: ( - source: string, - sourceHandle?: string | null, - target?: string | null, - ) => void; - deletePreviousEdgeOfClassificationNode: (connection: Connection) => void; - duplicateNode: (id: string, name: string) => void; - duplicateIterationNode: (id: string, name: string) => void; - deleteEdge: () => void; - deleteEdgeById: (id: string) => void; - deleteNodeById: (id: string) => void; - deleteIterationNodeById: (id: string) => void; - deleteEdgeBySourceAndSourceHandle: (connection: Partial) => void; - findNodeByName: (operatorName: Operator) => RAGFlowNodeType | undefined; - updateMutableNodeFormItem: (id: string, field: string, value: any) => void; - getOperatorTypeFromId: (id?: string | null) => string | undefined; - getParentIdById: (id?: string | null) => string | undefined; - updateNodeName: (id: string, name: string) => void; - generateNodeName: (name: string) => string; - setClickedNodeId: (id?: string) => void; -}; - -// this is our useStore hook that we can use in our components to get parts of the store and call actions -const useGraphStore = create()( - devtools( - immer((set, get) => ({ - nodes: [] as RAGFlowNodeType[], - edges: [] as Edge[], - selectedNodeIds: [] as string[], - selectedEdgeIds: [] as string[], - clickedNodeId: '', - onNodesChange: (changes) => { - set({ - nodes: applyNodeChanges(changes, get().nodes), - }); - }, - onEdgesChange: (changes: EdgeChange[]) => { - set({ - edges: applyEdgeChanges(changes, get().edges), - }); - }, - onConnect: (connection: Connection) => { - const { - deletePreviousEdgeOfClassificationNode, - updateFormDataOnConnect, - } = get(); - set({ - edges: addEdge(connection, get().edges), - }); - deletePreviousEdgeOfClassificationNode(connection); - updateFormDataOnConnect(connection); - }, - onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => { - set({ - selectedEdgeIds: edges.map((x) => x.id), - selectedNodeIds: nodes.map((x) => x.id), - }); - }, - setNodes: (nodes: RAGFlowNodeType[]) => { - set({ nodes }); - }, - setEdges: (edges: Edge[]) => { - set({ edges }); - }, - setEdgesByNodeId: (nodeId: string, currentDownstreamEdges: Edge[]) => { - const { edges, setEdges } = get(); - // the previous downstream edge of this node - const previousDownstreamEdges = edges.filter( - (x) => x.source === nodeId, - ); - const isDifferent = - previousDownstreamEdges.length !== currentDownstreamEdges.length || - !previousDownstreamEdges.every((x) => - currentDownstreamEdges.some( - (y) => - y.source === x.source && - y.target === x.target && - y.sourceHandle === x.sourceHandle, - ), - ) || - !currentDownstreamEdges.every((x) => - previousDownstreamEdges.some( - (y) => - y.source === x.source && - y.target === x.target && - y.sourceHandle === x.sourceHandle, - ), - ); - - const intersectionDownstreamEdges = intersectionWith( - previousDownstreamEdges, - currentDownstreamEdges, - isEdgeEqual, - ); - if (isDifferent) { - // other operator's edges - const irrelevantEdges = edges.filter((x) => x.source !== nodeId); - // the added downstream edges - const selfAddedDownstreamEdges = differenceWith( - currentDownstreamEdges, - intersectionDownstreamEdges, - isEdgeEqual, - ); - setEdges([ - ...irrelevantEdges, - ...intersectionDownstreamEdges, - ...selfAddedDownstreamEdges, - ]); - } - }, - addNode: (node: RAGFlowNodeType) => { - set({ nodes: get().nodes.concat(node) }); - }, - getNode: (id?: string | null) => { - return get().nodes.find((x) => x.id === id); - }, - getOperatorTypeFromId: (id?: string | null) => { - return get().getNode(id)?.data?.label; - }, - getParentIdById: (id?: string | null) => { - return get().getNode(id)?.parentId; - }, - addEdge: (connection: Connection) => { - set({ - edges: addEdge(connection, get().edges), - }); - get().deletePreviousEdgeOfClassificationNode(connection); - // TODO: This may not be reasonable. You need to choose between listening to changes in the form. - get().updateFormDataOnConnect(connection); - }, - getEdge: (id: string) => { - return get().edges.find((x) => x.id === id); - }, - updateFormDataOnConnect: (connection: Connection) => { - const { getOperatorTypeFromId, updateNodeForm, updateSwitchFormData } = - get(); - const { source, target, sourceHandle } = connection; - const operatorType = getOperatorTypeFromId(source); - if (source) { - switch (operatorType) { - case Operator.Relevant: - updateNodeForm(source, { [sourceHandle as string]: target }); - break; - case Operator.Categorize: - if (sourceHandle) - updateNodeForm(source, target, [ - 'category_description', - sourceHandle, - 'to', - ]); - break; - case Operator.Switch: { - updateSwitchFormData(source, sourceHandle, target); - break; - } - default: - break; - } - } - }, - deletePreviousEdgeOfClassificationNode: (connection: Connection) => { - // Delete the edge on the classification node or relevant node anchor when the anchor is connected to other nodes - const { edges, getOperatorTypeFromId, deleteEdgeById } = get(); - // the node containing the anchor - const anchoredNodes = [ - Operator.Categorize, - Operator.Relevant, - Operator.Switch, - ]; - if ( - anchoredNodes.some( - (x) => x === getOperatorTypeFromId(connection.source), - ) - ) { - const previousEdge = edges.find( - (x) => - x.source === connection.source && - x.sourceHandle === connection.sourceHandle && - x.target !== connection.target, - ); - if (previousEdge) { - deleteEdgeById(previousEdge.id); - } - } - }, - duplicateNode: (id: string, name: string) => { - const { getNode, addNode, generateNodeName, duplicateIterationNode } = - get(); - const node = getNode(id); - - if (node?.data.label === Operator.Iteration) { - duplicateIterationNode(id, name); - return; - } - - addNode({ - ...(node || {}), - data: { - ...duplicateNodeForm(node?.data), - name: generateNodeName(name), - }, - ...generateDuplicateNode(node?.position, node?.data?.label), - }); - }, - duplicateIterationNode: (id: string, name: string) => { - const { getNode, generateNodeName, nodes } = get(); - const node = getNode(id); - - const iterationNode: RAGFlowNodeType = { - ...(node || {}), - data: { - ...(node?.data || { label: Operator.Iteration, form: {} }), - name: generateNodeName(name), - }, - ...generateDuplicateNode(node?.position, node?.data?.label), - }; - - const children = nodes - .filter((x) => x.parentId === node?.id) - .map((x) => ({ - ...(x || {}), - data: { - ...duplicateNodeForm(x?.data), - name: generateNodeName(x.data.name), - }, - ...omit(generateDuplicateNode(x?.position, x?.data?.label), [ - 'position', - ]), - parentId: iterationNode.id, - })); - - set({ nodes: nodes.concat(iterationNode, ...children) }); - }, - deleteEdge: () => { - const { edges, selectedEdgeIds } = get(); - set({ - edges: edges.filter((edge) => - selectedEdgeIds.every((x) => x !== edge.id), - ), - }); - }, - deleteEdgeById: (id: string) => { - const { - edges, - updateNodeForm, - getOperatorTypeFromId, - updateSwitchFormData, - } = get(); - const currentEdge = edges.find((x) => x.id === id); - - if (currentEdge) { - const { source, sourceHandle } = currentEdge; - const operatorType = getOperatorTypeFromId(source); - // After deleting the edge, set the corresponding field in the node's form field to undefined - switch (operatorType) { - case Operator.Relevant: - updateNodeForm(source, { - [sourceHandle as string]: undefined, - }); - break; - case Operator.Categorize: - if (sourceHandle) - updateNodeForm(source, undefined, [ - 'category_description', - sourceHandle, - 'to', - ]); - break; - case Operator.Switch: { - updateSwitchFormData(source, sourceHandle, undefined); - break; - } - default: - break; - } - } - set({ - edges: edges.filter((edge) => edge.id !== id), - }); - }, - deleteEdgeBySourceAndSourceHandle: ({ - source, - sourceHandle, - }: Partial) => { - const { edges } = get(); - const nextEdges = edges.filter( - (edge) => - edge.source !== source || edge.sourceHandle !== sourceHandle, - ); - set({ - edges: nextEdges, - }); - }, - deleteNodeById: (id: string) => { - const { nodes, edges } = get(); - set({ - nodes: nodes.filter((node) => node.id !== id), - edges: edges - .filter((edge) => edge.source !== id) - .filter((edge) => edge.target !== id), - }); - }, - deleteIterationNodeById: (id: string) => { - const { nodes, edges } = get(); - const children = nodes.filter((node) => node.parentId === id); - set({ - nodes: nodes.filter((node) => node.id !== id && node.parentId !== id), - edges: edges.filter( - (edge) => - edge.source !== id && - edge.target !== id && - !children.some( - (child) => edge.source === child.id && edge.target === child.id, - ), - ), - }); - }, - findNodeByName: (name: Operator) => { - return get().nodes.find((x) => x.data.label === name); - }, - updateNodeForm: ( - nodeId: string, - values: any, - path: (string | number)[] = [], - ) => { - const nextNodes = get().nodes.map((node) => { - if (node.id === nodeId) { - let nextForm: Record = { ...node.data.form }; - if (path.length === 0) { - nextForm = Object.assign(nextForm, values); - } else { - lodashSet(nextForm, path, values); - } - return { - ...node, - data: { - ...node.data, - form: nextForm, - }, - } as any; - } - - return node; - }); - set({ - nodes: nextNodes, - }); - - return nextNodes; - }, - updateSwitchFormData: (source, sourceHandle, target) => { - const { updateNodeForm } = get(); - if (sourceHandle) { - if (sourceHandle === SwitchElseTo) { - updateNodeForm(source, target, [SwitchElseTo]); - } else { - const operatorIndex = getOperatorIndex(sourceHandle); - if (operatorIndex) { - updateNodeForm(source, target, [ - 'conditions', - Number(operatorIndex) - 1, // The index is the conditions form index - 'to', - ]); - } - } - } - }, - updateMutableNodeFormItem: (id: string, field: string, value: any) => { - const { nodes } = get(); - const idx = nodes.findIndex((x) => x.id === id); - if (idx) { - lodashSet(nodes, [idx, 'data', 'form', field], value); - } - }, - updateNodeName: (id, name) => { - if (id) { - set({ - nodes: get().nodes.map((node) => { - if (node.id === id) { - node.data.name = name; - } - - return node; - }), - }); - } - }, - setClickedNodeId: (id?: string) => { - set({ clickedNodeId: id }); - }, - generateNodeName: (name: string) => { - const { nodes } = get(); - - return generateNodeNamesWithIncreasingIndex(name, nodes); - }, - })), - { name: 'graph' }, - ), -); - -export default useGraphStore; diff --git a/web/src/pages/flow/utils.test.ts b/web/src/pages/flow/utils.test.ts deleted file mode 100644 index dbb89ce7e..000000000 --- a/web/src/pages/flow/utils.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import customer_service from '../../../../graph/test/dsl_examples/customer_service.json'; -import headhunter_zh from '../../../../graph/test/dsl_examples/headhunter_zh.json'; -import interpreter from '../../../../graph/test/dsl_examples/interpreter.json'; -import retrievalRelevantRewriteAndGenerate from '../../../../graph/test/dsl_examples/retrieval_relevant_rewrite_and_generate.json'; -import { dsl } from './mock'; -import { buildNodesAndEdgesFromDSLComponents } from './utils'; - -test('buildNodesAndEdgesFromDSLComponents', () => { - const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(dsl.components); - - expect(nodes.length).toEqual(4); - expect(edges.length).toEqual(4); - - expect(edges).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - source: 'begin', - target: 'Answer:China', - }), - expect.objectContaining({ - source: 'Answer:China', - target: 'Retrieval:China', - }), - expect.objectContaining({ - source: 'Retrieval:China', - target: 'Generate:China', - }), - expect.objectContaining({ - source: 'Generate:China', - target: 'Answer:China', - }), - ]), - ); -}); - -test('build nodes and edges from headhunter_zh dsl', () => { - const { edges, nodes } = buildNodesAndEdgesFromDSLComponents( - headhunter_zh.components, - ); - console.info('node length', nodes.length); - console.info('edge length', edges.length); - try { - fs.writeFileSync( - path.join(__dirname, 'headhunter_zh.json'), - JSON.stringify({ edges, nodes }, null, 4), - ); - console.log('JSON data is saved.'); - } catch (error) { - console.warn(error); - } - expect(nodes.length).toEqual(12); -}); - -test('build nodes and edges from customer_service dsl', () => { - const { edges, nodes } = buildNodesAndEdgesFromDSLComponents( - customer_service.components, - ); - console.info('node length', nodes.length); - console.info('edge length', edges.length); - try { - fs.writeFileSync( - path.join(__dirname, 'customer_service.json'), - JSON.stringify({ edges, nodes }, null, 4), - ); - console.log('JSON data is saved.'); - } catch (error) { - console.warn(error); - } - expect(nodes.length).toEqual(12); -}); - -test('build nodes and edges from interpreter dsl', () => { - const { edges, nodes } = buildNodesAndEdgesFromDSLComponents( - interpreter.components, - ); - console.info('node length', nodes.length); - console.info('edge length', edges.length); - try { - fs.writeFileSync( - path.join(__dirname, 'interpreter.json'), - JSON.stringify({ edges, nodes }, null, 4), - ); - console.log('JSON data is saved.'); - } catch (error) { - console.warn(error); - } - expect(nodes.length).toEqual(12); -}); - -test('build nodes and edges from chat bot dsl', () => { - const { edges, nodes } = buildNodesAndEdgesFromDSLComponents( - retrievalRelevantRewriteAndGenerate.components, - ); - try { - fs.writeFileSync( - path.join(__dirname, 'retrieval_relevant_rewrite_and_generate.json'), - JSON.stringify({ edges, nodes }, null, 4), - ); - console.log('JSON data is saved.'); - } catch (error) { - console.warn(error); - } - expect(nodes.length).toEqual(12); -}); diff --git a/web/src/pages/flow/utils.ts b/web/src/pages/flow/utils.ts deleted file mode 100644 index dcd0867dd..000000000 --- a/web/src/pages/flow/utils.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { - DSLComponents, - ICategorizeItem, - ICategorizeItemResult, - RAGFlowNodeType, -} from '@/interfaces/database/flow'; -import { removeUselessFieldsFromValues } from '@/utils/form'; -import { Edge, Node, Position, XYPosition } from '@xyflow/react'; -import { FormInstance, FormListFieldData } from 'antd'; -import { humanId } from 'human-id'; -import { curry, get, intersectionWith, isEqual, sample } from 'lodash'; -import pipe from 'lodash/fp/pipe'; -import isObject from 'lodash/isObject'; -import { v4 as uuidv4 } from 'uuid'; -import { - CategorizeAnchorPointPositions, - NoDebugOperatorsList, - NodeMap, - Operator, -} from './constant'; -import { IPosition } from './interface'; - -const buildEdges = ( - operatorIds: string[], - currentId: string, - allEdges: Edge[], - isUpstream = false, - componentName: string, - nodeParams: Record, -) => { - operatorIds.forEach((cur) => { - const source = isUpstream ? cur : currentId; - const target = isUpstream ? currentId : cur; - if (!allEdges.some((e) => e.source === source && e.target === target)) { - const edge: Edge = { - id: uuidv4(), - label: '', - // type: 'step', - source: source, - target: target, - // markerEnd: { - // type: MarkerType.ArrowClosed, - // color: 'rgb(157 149 225)', - // width: 20, - // height: 20, - // }, - }; - if (componentName === Operator.Categorize && !isUpstream) { - const categoryDescription = - nodeParams.category_description as ICategorizeItemResult; - - const name = Object.keys(categoryDescription).find( - (x) => categoryDescription[x].to === target, - ); - - if (name) { - edge.sourceHandle = name; - } - } - allEdges.push(edge); - } - }); -}; - -export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => { - const nodes: Node[] = []; - let edges: Edge[] = []; - - Object.entries(data).forEach(([key, value]) => { - const downstream = [...value.downstream]; - const upstream = [...value.upstream]; - const { component_name: componentName, params } = value.obj; - nodes.push({ - id: key, - type: NodeMap[value.obj.component_name as Operator] || 'ragNode', - position: { x: 0, y: 0 }, - data: { - label: componentName, - name: humanId(), - form: params, - }, - sourcePosition: Position.Left, - targetPosition: Position.Right, - }); - - buildEdges(upstream, key, edges, true, componentName, params); - buildEdges(downstream, key, edges, false, componentName, params); - }); - - return { nodes, edges }; -}; - -const buildComponentDownstreamOrUpstream = ( - edges: Edge[], - nodeId: string, - isBuildDownstream = true, -) => { - return edges - .filter((y) => y[isBuildDownstream ? 'source' : 'target'] === nodeId) - .map((y) => y[isBuildDownstream ? 'target' : 'source']); -}; - -const removeUselessDataInTheOperator = curry( - (operatorName: string, params: Record) => { - if ( - operatorName === Operator.Generate || - operatorName === Operator.Categorize - ) { - return removeUselessFieldsFromValues(params, ''); - } - return params; - }, -); -// initialize data for operators without parameters -// const initializeOperatorParams = curry((operatorName: string, values: any) => { -// if (isEmpty(values)) { -// return initialFormValuesMap[operatorName as Operator]; -// } -// return values; -// }); - -const buildOperatorParams = (operatorName: string) => - pipe( - removeUselessDataInTheOperator(operatorName), - // initializeOperatorParams(operatorName), // Final processing, for guarantee - ); - -// construct a dsl based on the node information of the graph -export const buildDslComponentsByGraph = ( - nodes: RAGFlowNodeType[], - edges: Edge[], - oldDslComponents: DSLComponents, -): DSLComponents => { - const components: DSLComponents = {}; - - nodes - ?.filter((x) => x.data.label !== Operator.Note) - .forEach((x) => { - const id = x.id; - const operatorName = x.data.label; - components[id] = { - obj: { - ...(oldDslComponents[id]?.obj ?? {}), - component_name: operatorName, - params: - buildOperatorParams(operatorName)( - x.data.form as Record, - ) ?? {}, - }, - downstream: buildComponentDownstreamOrUpstream(edges, id, true), - upstream: buildComponentDownstreamOrUpstream(edges, id, false), - parent_id: x?.parentId, - }; - }); - - return components; -}; - -export const receiveMessageError = (res: any) => - res && (res?.response.status !== 200 || res?.data?.code !== 0); - -// Replace the id in the object with text -export const replaceIdWithText = ( - obj: Record | unknown[] | unknown, - getNameById: (id?: string) => string | undefined, -) => { - if (isObject(obj)) { - const ret: Record | unknown[] = Array.isArray(obj) - ? [] - : {}; - Object.keys(obj).forEach((key) => { - const val = (obj as Record)[key]; - const text = typeof val === 'string' ? getNameById(val) : undefined; - (ret as Record)[key] = text - ? text - : replaceIdWithText(val, getNameById); - }); - - return ret; - } - - return obj; -}; - -export const isEdgeEqual = (previous: Edge, current: Edge) => - previous.source === current.source && - previous.target === current.target && - previous.sourceHandle === current.sourceHandle; - -export const buildNewPositionMap = ( - currentKeys: string[], - previousPositionMap: Record, -) => { - // index in use - const indexesInUse = Object.values(previousPositionMap).map((x) => x.idx); - const previousKeys = Object.keys(previousPositionMap); - const intersectionKeys = intersectionWith( - previousKeys, - currentKeys, - (categoryDataKey: string, positionMapKey: string) => - categoryDataKey === positionMapKey, - ); - // difference set - const currentDifferenceKeys = currentKeys.filter( - (x) => !intersectionKeys.some((y: string) => y === x), - ); - const newPositionMap = currentDifferenceKeys.reduce< - Record - >((pre, cur) => { - // take a coordinate - const effectiveIdxes = CategorizeAnchorPointPositions.map( - (x, idx) => idx, - ).filter((x) => !indexesInUse.some((y) => y === x)); - const idx = sample(effectiveIdxes); - if (idx !== undefined) { - indexesInUse.push(idx); - pre[cur] = { ...CategorizeAnchorPointPositions[idx], idx }; - } - - return pre; - }, {}); - - return { intersectionKeys, newPositionMap }; -}; - -export const isKeysEqual = (currentKeys: string[], previousKeys: string[]) => { - return isEqual(currentKeys.sort(), previousKeys.sort()); -}; - -export const getOperatorIndex = (handleTitle: string) => { - return handleTitle.split(' ').at(-1); -}; - -// Get the value of other forms except itself -export const getOtherFieldValues = ( - form: FormInstance, - formListName: string = 'items', - field: FormListFieldData, - latestField: string, -) => - (form.getFieldValue([formListName]) ?? []) - .map((x: any) => { - return get(x, latestField); - }) - .filter( - (x: string) => - x !== form.getFieldValue([formListName, field.name, latestField]), - ); - -export const generateSwitchHandleText = (idx: number) => { - return `Case ${idx + 1}`; -}; - -export const getNodeDragHandle = (nodeType?: string) => { - return nodeType === Operator.Note ? '.note-drag-handle' : undefined; -}; - -const splitName = (name: string) => { - const names = name.split('_'); - const type = names.at(0); - const index = Number(names.at(-1)); - - return { type, index }; -}; - -export const generateNodeNamesWithIncreasingIndex = ( - name: string, - nodes: RAGFlowNodeType[], -) => { - const templateNameList = nodes - .filter((x) => { - const temporaryName = x.data.name; - - const { type, index } = splitName(temporaryName); - - return ( - temporaryName.match(/_/g)?.length === 1 && - type === name && - !isNaN(index) - ); - }) - .map((x) => { - const temporaryName = x.data.name; - const { index } = splitName(temporaryName); - - return { - idx: index, - name: temporaryName, - }; - }) - .sort((a, b) => a.idx - b.idx); - - let index: number = 0; - for (let i = 0; i < templateNameList.length; i++) { - const idx = templateNameList[i]?.idx; - const nextIdx = templateNameList[i + 1]?.idx; - if (idx + 1 !== nextIdx) { - index = idx + 1; - break; - } - } - - return `${name}_${index}`; -}; - -export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => { - const form: Record = { ...(nodeData?.form ?? {}) }; - - // Delete the downstream node corresponding to the to field of the Categorize operator - if (nodeData?.label === Operator.Categorize) { - form.category_description = Object.keys(form.category_description).reduce< - Record> - >((pre, cur) => { - pre[cur] = { - ...form.category_description[cur], - to: undefined, - }; - return pre; - }, {}); - } - - // Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator - if (nodeData?.label === Operator.Relevant) { - form.yes = undefined; - form.no = undefined; - } - - return { - ...(nodeData ?? { label: '' }), - form, - }; -}; - -export const getDrawerWidth = () => { - return window.innerWidth > 1278 ? '40%' : 470; -}; - -export const needsSingleStepDebugging = (label: string) => { - return !NoDebugOperatorsList.some((x) => (label as Operator) === x); -}; - -// Get the coordinates of the node relative to the Iteration node -export function getRelativePositionToIterationNode( - nodes: RAGFlowNodeType[], - position?: XYPosition, // relative position -) { - if (!position) { - return; - } - - const iterationNodes = nodes.filter( - (node) => node.data.label === Operator.Iteration, - ); - - for (const iterationNode of iterationNodes) { - const { - position: { x, y }, - width, - height, - } = iterationNode; - const halfWidth = (width || 0) / 2; - if ( - position.x >= x - halfWidth && - position.x <= x + halfWidth && - position.y >= y && - position.y <= y + (height || 0) - ) { - return { - parentId: iterationNode.id, - position: { x: position.x - x + halfWidth, y: position.y - y }, - }; - } - } -} - -export const generateDuplicateNode = ( - position?: XYPosition, - label?: string, -) => { - const nextPosition = { - x: (position?.x || 0) + 50, - y: (position?.y || 0) + 50, - }; - - return { - selected: false, - dragging: false, - id: `${label}:${humanId()}`, - position: nextPosition, - dragHandle: getNodeDragHandle(label), - }; -}; - -/** - * convert the following object into a list - * - * { - "product_related": { - "description": "The question is about product usage, appearance and how it works.", - "examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?", - "to": "generate:0" - } - } -*/ -export const buildCategorizeListFromObject = ( - categorizeItem: ICategorizeItemResult, -) => { - // Categorize's to field has two data sources, with edges as the data source. - // Changes in the edge or to field need to be synchronized to the form field. - return Object.keys(categorizeItem) - .reduce>((pre, cur) => { - // synchronize edge data to the to field - - pre.push({ name: cur, ...categorizeItem[cur] }); - return pre; - }, []) - .sort((a, b) => a.index - b.index); -}; diff --git a/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx b/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx index c878d917d..68dee7aa6 100644 --- a/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx +++ b/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx @@ -122,7 +122,11 @@ export function EditMcpDialog({ setFieldChanged={setFieldChanged} > {nextTools?.length || 0} tools available} + title={ +
+ {nextTools?.length || 0} {t('mcp.toolsAvailable')} +
+ } open={collapseOpen} onOpenChange={setCollapseOpen} rightContent={ diff --git a/web/src/pages/profile-setting/mcp/index.tsx b/web/src/pages/profile-setting/mcp/index.tsx index 912d71cb6..440d3579a 100644 --- a/web/src/pages/profile-setting/mcp/index.tsx +++ b/web/src/pages/profile-setting/mcp/index.tsx @@ -33,10 +33,10 @@ export default function McpServer() { return (
-
MCP Servers
+
{t('mcp.mcpServers')}
- Customize the list of MCP servers + {t('mcp.customizeTheListOfMcpServers')}
(methods); @@ -131,4 +134,8 @@ export const fetchAgentLogsByCanvasId = ( return request.get(methods.fetchAgentLogs.url(canvasId), { params: params }); }; +export const fetchPipeLineList = (params: IPipeLineListRequest) => { + return request.get(api.listCanvas, { params: params }); +}; + export default agentService; diff --git a/web/src/services/dataflow-service.ts b/web/src/services/dataflow-service.ts new file mode 100644 index 000000000..6c3b21f3e --- /dev/null +++ b/web/src/services/dataflow-service.ts @@ -0,0 +1,37 @@ +import api from '@/utils/api'; +import { registerNextServer } from '@/utils/register-server'; + +const { + listDataflow, + removeDataflow, + fetchDataflow, + runDataflow, + setDataflow, +} = api; + +const methods = { + listDataflow: { + url: listDataflow, + method: 'get', + }, + removeDataflow: { + url: removeDataflow, + method: 'post', + }, + fetchDataflow: { + url: fetchDataflow, + method: 'get', + }, + runDataflow: { + url: runDataflow, + method: 'post', + }, + setDataflow: { + url: setDataflow, + method: 'post', + }, +} as const; + +const dataflowService = registerNextServer(methods); + +export default dataflowService; diff --git a/web/src/services/knowledge-service.ts b/web/src/services/knowledge-service.ts index 8d0bb8d30..350fa4e2a 100644 --- a/web/src/services/knowledge-service.ts +++ b/web/src/services/knowledge-service.ts @@ -4,6 +4,7 @@ import { IFetchKnowledgeListRequestBody, IFetchKnowledgeListRequestParams, } from '@/interfaces/request/knowledge'; +import { ProcessingType } from '@/pages/dataset/dataset-overview/dataset-common'; import api from '@/utils/api'; import registerServer from '@/utils/register-server'; import request, { post } from '@/utils/request'; @@ -39,6 +40,13 @@ const { setMeta, getMeta, retrievalTestShare, + getKnowledgeBasicInfo, + fetchDataPipelineLog, + fetchPipelineDatasetLogs, + runGraphRag, + traceGraphRag, + runRaptor, + traceRaptor, } = api; const methods = { @@ -169,6 +177,43 @@ const methods = { url: retrievalTestShare, method: 'post', }, + getKnowledgeBasicInfo: { + url: getKnowledgeBasicInfo, + method: 'get', + }, + fetchDataPipelineLog: { + url: fetchDataPipelineLog, + method: 'post', + }, + fetchPipelineDatasetLogs: { + url: fetchPipelineDatasetLogs, + method: 'post', + }, + get_pipeline_detail: { + url: api.get_pipeline_detail, + method: 'get', + }, + + runGraphRag: { + url: runGraphRag, + method: 'post', + }, + traceGraphRag: { + url: traceGraphRag, + method: 'get', + }, + runRaptor: { + url: runRaptor, + method: 'post', + }, + traceRaptor: { + url: traceRaptor, + method: 'get', + }, + pipelineRerun: { + url: api.pipelineRerun, + method: 'post', + }, }; const kbService = registerServer(methods, request); @@ -205,4 +250,23 @@ export const listDocument = ( export const documentFilter = (kb_id: string) => request.post(api.get_dataset_filter, { kb_id }); +export const listDataPipelineLogDocument = ( + params?: IFetchKnowledgeListRequestParams, + body?: IFetchDocumentListRequestBody, +) => request.post(api.fetchDataPipelineLog, { data: body || {}, params }); +export const listPipelineDatasetLogs = ( + params?: IFetchKnowledgeListRequestParams, + body?: IFetchDocumentListRequestBody, +) => request.post(api.fetchPipelineDatasetLogs, { data: body || {}, params }); + +export function deletePipelineTask({ + kb_id, + type, +}: { + kb_id: string; + type: ProcessingType; +}) { + return request.delete(api.unbindPipelineTask({ kb_id, type })); +} + export default kbService; diff --git a/web/src/stories/slider-input-form-field.stories.tsx b/web/src/stories/slider-input-form-field.stories.tsx new file mode 100644 index 000000000..90bb13260 --- /dev/null +++ b/web/src/stories/slider-input-form-field.stories.tsx @@ -0,0 +1,171 @@ +import { Form } from '@/components/ui/form'; +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { useForm } from 'react-hook-form'; + +import { SliderInputFormField } from '@/components/slider-input-form-field'; +import { FormLayout } from '@/constants/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/SliderInputFormField', + component: SliderInputFormField, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +## Component Description + +SliderInputFormField is a form field component that combines a slider and a numeric input field. +It provides a user-friendly way to select numeric values within a specified range. `, + }, + }, + }, + tags: ['autodocs'], + argTypes: { + name: { control: 'text' }, + label: { control: 'text' }, + min: { control: 'number' }, + max: { control: 'number' }, + step: { control: 'number' }, + defaultValue: { control: 'number' }, + layout: { + control: 'select', + options: [FormLayout.Vertical, FormLayout.Horizontal], + }, + }, + args: { + name: 'sliderValue', + label: 'Slider Value', + min: 0, + max: 100, + step: 1, + defaultValue: 50, + }, +} satisfies Meta; + +// Form wrapper decorator +const WithFormProvider = ({ children }: { children: React.ReactNode }) => { + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(z.object({})), + }); + return
{children}
; +}; + +const withFormProvider = (Story: any) => ( + + + +); + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Default: Story = { + decorators: [withFormProvider], + args: { + name: 'sliderValue', + label: 'Slider Value', + min: 0, + max: 100, + step: 1, + defaultValue: 50, + }, + parameters: { + docs: { + description: { + story: ` +### Basic Usage + +\`\`\`tsx +import { SliderInputFormField } from '@/components/slider-input-form-field'; + + +\`\`\` + `, + }, + }, + }, +}; + +export const HorizontalLayout: Story = { + decorators: [withFormProvider], + args: { + name: 'horizontalSlider', + label: 'Horizontal Slider', + min: 0, + max: 200, + step: 5, + defaultValue: 100, + layout: FormLayout.Horizontal, + }, + parameters: { + docs: { + description: { + story: ` +### Horizontal Layout + +\`\`\`tsx +import { SliderInputFormField } from '@/components/slider-input-form-field'; +import { FormLayout } from '@/constants/form'; + + +\`\`\` + `, + }, + }, + }, +}; + +export const CustomRange: Story = { + decorators: [withFormProvider], + args: { + name: 'customRange', + label: 'Custom Range (0-1000)', + min: 0, + max: 1000, + step: 10, + defaultValue: 500, + }, + parameters: { + docs: { + description: { + story: ` +### Custom Range + +\`\`\`tsx +import { SliderInputFormField } from '@/components/slider-input-form-field'; + + +\`\`\` + `, + }, + }, + }, +}; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index cf294a67c..273501a2a 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -45,6 +45,18 @@ export default { getKnowledgeGraph: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/knowledge_graph`, getMeta: `${api_host}/kb/get_meta`, + getKnowledgeBasicInfo: `${api_host}/kb/basic_info`, + // data pipeline log + fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`, + get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`, + fetchPipelineDatasetLogs: `${api_host}/kb/list_pipeline_dataset_logs`, + runGraphRag: `${api_host}/kb/run_graphrag`, + traceGraphRag: `${api_host}/kb/trace_graphrag`, + runRaptor: `${api_host}/kb/run_raptor`, + traceRaptor: `${api_host}/kb/trace_raptor`, + unbindPipelineTask: ({ kb_id, type }: { kb_id: string; type: string }) => + `${api_host}/kb/unbind_task?kb_id=${kb_id}&pipeline_task_type=${type}`, + pipelineRerun: `${api_host}/canvas/rerun`, // tags listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, @@ -138,7 +150,6 @@ export default { // flow listTemplates: `${api_host}/canvas/templates`, listCanvas: `${api_host}/canvas/list`, - listCanvasTeam: `${api_host}/canvas/listteam`, getCanvas: `${api_host}/canvas/get`, getCanvasSSE: `${api_host}/canvas/getsse`, removeCanvas: `${api_host}/canvas/rm`, @@ -165,6 +176,8 @@ export default { fetchExternalAgentInputs: (canvasId: string) => `${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`, prompt: `${api_host}/canvas/prompts`, + cancelDataflow: (id: string) => `${api_host}/canvas/cancel/${id}`, + downloadFile: `${api_host}/canvas/download`, // mcp server listMcpServer: `${api_host}/mcp_server/list`, @@ -190,4 +203,11 @@ export default { mindmapShare: `${ExternalApi}${api_host}/searchbots/mindmap`, getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`, retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`, + + // data pipeline + fetchDataflow: (id: string) => `${api_host}/dataflow/get/${id}`, + setDataflow: `${api_host}/dataflow/set`, + removeDataflow: `${api_host}/dataflow/rm`, + listDataflow: `${api_host}/dataflow/list`, + runDataflow: `${api_host}/dataflow/run`, }; diff --git a/web/src/utils/canvas-util.tsx b/web/src/utils/canvas-util.tsx new file mode 100644 index 000000000..bfe6165b1 --- /dev/null +++ b/web/src/utils/canvas-util.tsx @@ -0,0 +1,75 @@ +import { BaseNode } from '@/interfaces/database/agent'; +import { Edge } from '@xyflow/react'; +import { isEmpty } from 'lodash'; +import { ComponentType, ReactNode } from 'react'; + +export 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 buildNodeOutputOptions({ + nodes, + edges, + nodeId, + Icon, +}: { + nodes: BaseNode[]; + edges: Edge[]; + nodeId?: string; + Icon: ComponentType<{ name: string }>; +}) { + 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, + , + ), + })); +} diff --git a/web/src/utils/common-util.ts b/web/src/utils/common-util.ts index aa37e2950..879848a8f 100644 --- a/web/src/utils/common-util.ts +++ b/web/src/utils/common-util.ts @@ -154,8 +154,11 @@ function getCSSVariableValue(variableName: string): string { return value; } -// Parse the color and convert to RGBA -export function parseColorToRGBA(color: string): [number, number, number] { +/**Parse the color and convert to RGB, + * #fff -> [255, 255, 255] + * var(--text-primary) -> [var(--text-primary-r), var(--text-primary-g), var(--text-primary-b)] + * */ +export function parseColorToRGB(color: string): [number, number, number] { // Handling CSS variables (e.g. var(--accent-primary)) let colorStr = color; if (colorStr.startsWith('var(')) { @@ -172,6 +175,32 @@ export function parseColorToRGBA(color: string): [number, number, number] { colorStr = getCSSVariableValue(varName); } + // Handle rgb(var(--accent-primary)) format + if (colorStr.startsWith('rgb(var(')) { + const varMatch = colorStr.match(/rgb\(var\(([^)]+)\)\)/); + if (!varMatch) { + console.error(`Invalid nested CSS variable: ${color}`); + return [0, 0, 0]; + } + const varName = varMatch[1]; + if (!varName) { + console.error(`Invalid nested CSS variable: ${colorStr}`); + return [0, 0, 0]; + } + // Get the CSS variable value which should be in format "r, g, b" + const rgbValues = getCSSVariableValue(varName); + const rgbMatch = rgbValues.match(/^(\d+),?\s*(\d+),?\s*(\d+)$/); + if (rgbMatch) { + return [ + parseInt(rgbMatch[1]), + parseInt(rgbMatch[2]), + parseInt(rgbMatch[3]), + ]; + } + console.error(`Unsupported RGB CSS variable format: ${rgbValues}`); + return [0, 0, 0]; + } + // Handles hexadecimal colors (e.g. #FF5733) if (colorStr.startsWith('#')) { const cleanedHex = colorStr.replace(/^#/, ''); @@ -205,3 +234,14 @@ export function parseColorToRGBA(color: string): [number, number, number] { console.error(`Unsupported colorStr format: ${colorStr}`); return [0, 0, 0]; } + +/** + * + * @param color eg: #fff, or var(--color-text-primary) + * @param opcity 0~1 + * @return rgba(r,g,b,opcity) + */ +export function parseColorToRGBA(color: string, opcity = 1): string { + const [r, g, b] = parseColorToRGB(color); + return `rgba(${r},${g},${b},${opcity})`; +} diff --git a/web/src/utils/component-util.ts b/web/src/utils/component-util.ts index 724be1caa..a28d3996d 100644 --- a/web/src/utils/component-util.ts +++ b/web/src/utils/component-util.ts @@ -1,3 +1,13 @@ -export function buildSelectOptions(list: Array) { +export function buildSelectOptions( + list: Array, + keyName?: string, + valueName?: string, +) { + if (!Array.isArray(list) || !list.length) { + return []; + } + if (keyName && valueName) { + return list.map((x) => ({ label: x[valueName], value: x[keyName] })); + } return list.map((x) => ({ label: x, value: x })); } diff --git a/web/src/utils/dataset-util.ts b/web/src/utils/dataset-util.ts index 6a5d95104..b8e8df7d7 100644 --- a/web/src/utils/dataset-util.ts +++ b/web/src/utils/dataset-util.ts @@ -7,27 +7,3 @@ export function isKnowledgeGraphParser(parserId: DocumentParserType) { export function isNaiveParser(parserId: DocumentParserType) { return parserId === DocumentParserType.Naive; } - -export type FilterType = { - id: string; - label: string; - count: number; -}; - -export function groupListByType>( - list: T[], - idField: string, - labelField: string, -) { - const fileTypeList: FilterType[] = []; - list.forEach((x) => { - const item = fileTypeList.find((y) => y.id === x[idField]); - if (!item) { - fileTypeList.push({ id: x[idField], label: x[labelField], count: 1 }); - } else { - item.count += 1; - } - }); - - return fileTypeList; -} diff --git a/web/src/utils/date.ts b/web/src/utils/date.ts index 56feb940c..1566c8193 100644 --- a/web/src/utils/date.ts +++ b/web/src/utils/date.ts @@ -43,3 +43,21 @@ export function formatStandardDate(date: any) { } return parsedDate.format('YYYY-MM-DD'); } + +export function formatSecondsToHumanReadable(seconds: number): string { + if (isNaN(seconds) || seconds < 0) { + return '0s'; + } + + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + // const s = toFixed(seconds % 60, 3); + const s = seconds % 60; + const formattedSeconds = s === 0 ? '0' : s.toFixed(3).replace(/\.?0+$/, ''); + const parts = []; + if (h > 0) parts.push(`${h}h `); + if (m > 0) parts.push(`${m}m `); + if (s || parts.length === 0) parts.push(`${formattedSeconds}s`); + + return parts.join(''); +} diff --git a/web/src/utils/form.ts b/web/src/utils/form.ts index 002409435..ecb56c424 100644 --- a/web/src/utils/form.ts +++ b/web/src/utils/form.ts @@ -35,7 +35,9 @@ export function buildOptions( ) { if (t) { return Object.values(data).map((val) => ({ - label: t(`${prefix ? prefix + '.' : ''}${val.toLowerCase()}`), + label: t( + `${prefix ? prefix + '.' : ''}${typeof val === 'string' ? val.toLowerCase() : val}`, + ), value: val, })); } diff --git a/web/src/utils/list-filter-util.ts b/web/src/utils/list-filter-util.ts new file mode 100644 index 000000000..727f55e9b --- /dev/null +++ b/web/src/utils/list-filter-util.ts @@ -0,0 +1,29 @@ +export type FilterType = { + id: string; + label: string; + count: number; +}; + +export function groupListByType>( + list: T[], + idField: string, + labelField: string, +) { + const fileTypeList: FilterType[] = []; + list.forEach((x) => { + const item = fileTypeList.find((y) => y.id === x[idField]); + if (!item) { + fileTypeList.push({ id: x[idField], label: x[labelField], count: 1 }); + } else { + item.count += 1; + } + }); + + return fileTypeList; +} + +export function buildOwnersFilter>(list: T[]) { + const owners = groupListByType(list, 'tenant_id', 'nickname'); + + return { field: 'owner', list: owners, label: 'Owner' }; +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index c04ca939d..17a8acfc3 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -28,7 +28,7 @@ module.exports = { }, extend: { colors: { - border: 'var(--colors-outline-neutral-strong)', + border: 'var(--border-default)', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', background: 'var(--background)', @@ -76,11 +76,23 @@ module.exports = { 'border-default': 'var(--border-default)', 'border-accent': 'var(--border-accent)', 'border-button': 'var(--border-button)', - 'accent-primary': 'var(--accent-primary)', + 'accent-primary': { + DEFAULT: 'rgb(var(--accent-primary) / )', + 5: 'rgba(var(--accent-primary) / 0.05)', // 5% + }, 'bg-accent': 'var(--bg-accent)', - 'state-success': 'var(--state-success)', - 'state-warning': 'var(--state-warning)', - 'state-error': 'var(--state-error)', + 'state-success': { + DEFAULT: 'rgb(var(--state-success) / )', + 5: 'rgba(var(--state-success) / 0.05)', // 5% + }, + 'state-warning': { + DEFAULT: 'rgb(var(--state-warning) / )', + 5: 'rgba(var(--state-warning) / 0.05)', // 5% + }, + 'state-error': { + DEFAULT: 'rgb(var(--state-error) / )', + 5: 'rgba(var(--state-error) / 0.05)', // 5% + }, 'team-group': 'var(--team-group)', 'team-member': 'var(--team-member)', 'team-department': 'var(--team-department)', diff --git a/web/tailwind.css b/web/tailwind.css index 9df292555..fc96e6232 100644 --- a/web/tailwind.css +++ b/web/tailwind.css @@ -100,7 +100,6 @@ --bg-card: rgba(0, 0, 0, 0.05); --bg-component: #ffffff; --bg-input: rgba(255, 255, 255, 0); - --bg-accent: rgba(76, 164, 231, 0.05); /* Button ,Body text, Input completed text */ --text-primary: #161618; --text-secondary: #75787a; @@ -113,13 +112,15 @@ --border-accent: #000000; --border-button: rgba(0, 0, 0, 0.1); /* Regulators, parsing, switches, variables */ - --accent-primary: #00beb4; + --accent-primary: 0 190 180; /* Output Variables Box */ --bg-accent: rgba(76, 164, 231, 0.05); - --state-success: #3ba05c; - --state-warning: #faad14; - --state-error: #d8494b; + /* --state-success: #3ba05c; */ + --state-success: 59 160 92; + /* --state-warning: #767573; */ + --state-warning: 118 117 115; + --state-error: 216 73 75; --team-group: #5ab77e; --team-member: #5c96c8; @@ -291,8 +292,8 @@ @layer utilities { .scrollbar-auto { /* hide scrollbar */ - scrollbar-width: none; - scrollbar-color: transparent transparent; + scrollbar-width: thin; + scrollbar-color: var(--border-default) var(--bg-card); } .scrollbar-auto::-webkit-scrollbar {