├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── ai-flow-pro-homepage.png ├── comparison-pro-vs-opensource-v2.png ├── flow-example-2.png ├── flow-example.png ├── gpt-vision.png ├── header.png ├── intro.png ├── layout.png ├── predefined-prompts.png ├── presentation.png ├── replicate-models.png ├── scenario-1-1.png ├── scenario-1-2.png ├── scenario-example.png ├── split-input.png └── summary.png ├── bin └── generate_python_classes_from_ts.sh ├── docker ├── README.md ├── docker-compose.it.yml ├── docker-compose.yml └── healthcheck.sh ├── integration_tests ├── .gitignore ├── package-lock.json ├── package.json ├── tests │ ├── nodeProcessingOrder │ │ ├── nodeErrorTest.ts │ │ ├── nodeParallelExecutionDurationTest.ts │ │ ├── nodeWithChildrenTest.ts │ │ ├── nodeWithMultipleParentsTest.ts │ │ ├── nodesWithoutLinkTest.ts │ │ └── singleNodeTest.ts │ └── socketEvents │ │ ├── processFileEventTest.ts │ │ ├── runNodeEventTest.ts │ │ └── socketConnectionTest.ts ├── tsconfig.json └── utils │ ├── requestDatas.ts │ └── testHooks.ts └── packages ├── backend ├── .env ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── env_config.py │ ├── flask │ │ ├── app_routes │ │ │ ├── __init__.py │ │ │ ├── image_routes.py │ │ │ ├── node_routes.py │ │ │ ├── parameters_routes.py │ │ │ ├── static_routes.py │ │ │ ├── template_routes.py │ │ │ └── upload_routes.py │ │ ├── decorators.py │ │ ├── flask_app.py │ │ ├── routes.py │ │ ├── socketio_init.py │ │ ├── sockets.py │ │ ├── utils │ │ │ └── constants.py │ │ └── validators.py │ ├── llms │ │ ├── factory │ │ │ ├── llm_factory.py │ │ │ └── paid_api_llm_factory.py │ │ ├── prompt_engine │ │ │ ├── prompt_engine.py │ │ │ ├── simple_prompt_engine.py │ │ │ └── vector_index_prompt_engine.py │ │ └── utils │ │ │ └── max_token_for_model.py │ ├── log_config.py │ ├── processors │ │ ├── components │ │ │ ├── __init__.py │ │ │ ├── core │ │ │ │ ├── __init__.py │ │ │ │ ├── ai_data_splitter_processor.py │ │ │ │ ├── dall_e_prompt_processor.py │ │ │ │ ├── display_processor.py │ │ │ │ ├── file_processor.py │ │ │ │ ├── gpt_vision_processor.py │ │ │ │ ├── input_image_processor.py │ │ │ │ ├── input_processor.py │ │ │ │ ├── llm_prompt_processor.py │ │ │ │ ├── merge_processor.py │ │ │ │ ├── processor_type_name_utils.py │ │ │ │ ├── replicate_processor.py │ │ │ │ ├── stable_diffusion_stabilityai_prompt_processor.py │ │ │ │ ├── stable_video_diffusion_replicate.py │ │ │ │ ├── transition_processor.py │ │ │ │ ├── url_input_processor.py │ │ │ │ └── youtube_transcript_input_processor.py │ │ │ ├── extension │ │ │ │ ├── __init__.py │ │ │ │ ├── claude_anthropic_processor.py │ │ │ │ ├── deepseek_processor.py │ │ │ │ ├── document_to_text_processor.py │ │ │ │ ├── extension_processor.py │ │ │ │ ├── generate_number_processor.py │ │ │ │ ├── gpt_image_processor.py │ │ │ │ ├── http_get_processor.py │ │ │ │ ├── open_router_processor.py │ │ │ │ ├── openai_reasoning_processor.py │ │ │ │ ├── openai_text_to_speech_processor.py │ │ │ │ ├── replace_text_processor.py │ │ │ │ ├── stabilityai_generic_processor.py │ │ │ │ └── stable_diffusion_three_processor.py │ │ │ ├── model.py │ │ │ ├── node_config_builder.py │ │ │ ├── node_config_utils.py │ │ │ └── processor.py │ │ ├── context │ │ │ ├── processor_context.py │ │ │ └── processor_context_flask_request.py │ │ ├── factory │ │ │ ├── processor_factory.py │ │ │ └── processor_factory_iter_modules.py │ │ ├── launcher │ │ │ ├── abstract_topological_processor_launcher.py │ │ │ ├── async_processor_launcher.py │ │ │ ├── basic_processor_launcher.py │ │ │ ├── event_type.py │ │ │ ├── processor_event.py │ │ │ ├── processor_launcher.py │ │ │ └── processor_launcher_event.py │ │ └── observer │ │ │ ├── observer.py │ │ │ └── socketio_event_emitter.py │ ├── providers │ │ └── template │ │ │ ├── static_template_provider.py │ │ │ └── template_provider.py │ ├── root_injector.py │ ├── storage │ │ ├── local_storage_strategy.py │ │ ├── s3_storage_strategy.py │ │ └── storage_strategy.py │ ├── tasks │ │ ├── green_pool_task_manager.py │ │ ├── single_thread_tasks │ │ │ └── browser │ │ │ │ ├── async_browser_task.py │ │ │ │ └── browser_task.py │ │ ├── task_exception.py │ │ ├── task_manager.py │ │ ├── task_utils.py │ │ └── thread_pool_task_manager.py │ └── utils │ │ ├── node_extension_utils.py │ │ ├── openapi_client.py │ │ ├── openapi_converter.py │ │ ├── openapi_reader.py │ │ ├── processor_utils.py │ │ ├── replicate_utils.py │ │ └── web_scrapping │ │ ├── async_browser_manager.py │ │ └── browser_manager.py ├── config.yaml ├── hooks │ └── hook-app.processors.py ├── poetry.lock ├── pyproject.toml ├── requirements_windows.txt ├── resources │ ├── data │ │ └── openrouter_models.json │ └── openapi │ │ └── stabilityai.json ├── server.py ├── templates │ ├── template_1.json │ ├── template_10.json │ ├── template_11.json │ ├── template_12.json │ ├── template_13.json │ ├── template_14.json │ ├── template_2.json │ ├── template_3.json │ ├── template_4.json │ ├── template_5.json │ ├── template_6.json │ ├── template_7.json │ ├── template_8.json │ └── template_9.json └── tests │ ├── unit │ ├── test_ai_data_splitter.py │ ├── test_llm_prompt_processor.py │ ├── test_processor_factory.py │ ├── test_processor_launcher.py │ └── test_stable_diffusion_stabilityai_prompt_processor.py │ └── utils │ ├── llm_factory_mock.py │ ├── llm_mock.py │ ├── openai_mock_utils.py │ ├── processor_context_mock.py │ └── processor_factory_mock.py └── ui ├── .env ├── .gitignore ├── .prettierignore ├── Dockerfile ├── README.md ├── amplify ├── .config │ └── project-config.json ├── README.md ├── backend │ ├── auth │ │ └── aiflowfa7513ae │ │ │ └── cli-inputs.json │ ├── backend-config.json │ ├── tags.json │ └── types │ │ └── amplify-dependent-resources-ref.d.ts ├── cli.json ├── hooks │ └── README.md └── team-provider-info.json ├── index.html ├── jest.config.ts ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── postcss.config.js ├── prettier.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── backgrounds │ ├── g-particles.png │ └── g-simple.png ├── curve-edge.svg ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── gif-new-version.gif ├── handle-bottom-out.svg ├── handle-bottom.svg ├── handle-left-out.svg ├── handle-left.svg ├── handle-right-out.svg ├── handle-right.svg ├── handle-top-out.svg ├── handle-top.svg ├── health ├── img │ ├── anthropic-logo.svg │ ├── deepseek-logo.png │ ├── openai-white-logomark.svg │ ├── openrouter-logo.jpg │ ├── replicate-logo.png │ ├── stabilityai-logo.jpg │ └── youtube-logo.svg ├── locales │ ├── en │ │ ├── aiActions.json │ │ ├── config.json │ │ ├── dialogs.json │ │ ├── flow.json │ │ ├── nodeHelp.json │ │ ├── tips.json │ │ ├── tour.json │ │ └── version.json │ └── fr │ │ ├── aiActions.json │ │ ├── config.json │ │ ├── dialogs.json │ │ ├── flow.json │ │ ├── nodeHelp.json │ │ ├── tips.json │ │ ├── tour.json │ │ └── version.json ├── logo.png ├── logo.svg ├── robots.txt ├── samples │ └── intro.json ├── site.webmanifest ├── smooth-step-edge.svg ├── step-edge.svg ├── straight-edge.svg └── tour-assets │ ├── tour-step-connect-nodes.gif │ ├── tour-step-drag-and-drop.gif │ ├── tour-step-replicate-node.gif │ └── tour-step-run-node.gif ├── src ├── App.tsx ├── Main.tsx ├── api │ ├── cache │ │ ├── cacheManager.ts │ │ └── withCache.ts │ ├── client.ts │ ├── nodes.ts │ ├── parameters.ts │ ├── replicateModels.ts │ └── uploadFile.ts ├── components │ ├── Flow.tsx │ ├── LoadingScreen.tsx │ ├── bars │ │ ├── Sidebar.tsx │ │ └── dnd-sidebar │ │ │ ├── DnDSidebar.tsx │ │ │ ├── DraggableNode.tsx │ │ │ ├── GripIcon.tsx │ │ │ └── Section.tsx │ ├── buttons │ │ ├── ButtonRunAll.tsx │ │ ├── ConfigurationButton.tsx │ │ ├── DefaultSwitch.tsx │ │ └── EdgeTypeButton.tsx │ ├── edges │ │ └── buttonEdge.tsx │ ├── handles │ │ └── HandleWrapper.tsx │ ├── inputs │ │ └── InputWithButton.tsx │ ├── nodes │ │ ├── AIDataSplitterNode.tsx │ │ ├── DisplayNode.tsx │ │ ├── DynamicAPINode.tsx │ │ ├── FileUploadNode.tsx │ │ ├── GenericNode.tsx │ │ ├── Node.styles.ts │ │ ├── NodeHelpPopover.tsx │ │ ├── NodeWrapper.tsx │ │ ├── ReplicateNode.tsx │ │ ├── TransitionNode.tsx │ │ ├── node-button │ │ │ ├── InputNameBar.tsx │ │ │ └── NodePlayButton.tsx │ │ ├── node-input │ │ │ ├── FileUploadField.tsx │ │ │ ├── ImageMaskCreator.tsx │ │ │ ├── ImageMaskCreatorField.tsx │ │ │ ├── ImageMaskCreatorFieldFlowAware.tsx │ │ │ ├── KeyValueInputList.tsx │ │ │ ├── NodeField.tsx │ │ │ ├── NodeTextField.tsx │ │ │ ├── NodeTextarea.tsx │ │ │ ├── OutputRenderer.tsx │ │ │ └── TextAreaPopupWrapper.tsx │ │ ├── node-output │ │ │ ├── AudioUrlOutput.tsx │ │ │ ├── ImageBase64Output.tsx │ │ │ ├── ImageUrlOutput.tsx │ │ │ ├── MarkdownOutput.tsx │ │ │ ├── NodeOutput.tsx │ │ │ ├── OutputDisplay.tsx │ │ │ ├── PdfUrlOutput.tsx │ │ │ ├── ThreeDimensionalUrlOutput.tsx │ │ │ ├── VideoUrlOutput.tsx │ │ │ └── outputUtils.ts │ │ ├── types │ │ │ └── node.ts │ │ └── utils │ │ │ ├── HintComponent.tsx │ │ │ ├── ImageModal.tsx │ │ │ ├── ImageZoomable.tsx │ │ │ ├── NodeHelp.tsx │ │ │ ├── NodeIcons.tsx │ │ │ └── TextareaModal.tsx │ ├── players │ │ └── VideoJS.tsx │ ├── popups │ │ ├── ConfirmPopup.tsx │ │ ├── DefaultPopup.tsx │ │ ├── HelpPopup.tsx │ │ ├── UserMessagePopup.tsx │ │ ├── config-popup │ │ │ ├── AppParameters.tsx │ │ │ ├── ConfigPopup.tsx │ │ │ ├── DisplayParameters.tsx │ │ │ ├── ParametersFields.tsx │ │ │ ├── UserParameters.tsx │ │ │ ├── configMetadata.ts │ │ │ └── parameters.ts │ │ ├── select-model-popup │ │ │ ├── Model.tsx │ │ │ └── SelectModelPopup.tsx │ │ └── shared │ │ │ ├── FilterGrid.tsx │ │ │ ├── Grid.tsx │ │ │ └── LoadMoreButton.tsx │ ├── selectors │ │ ├── ActionGroup.tsx │ │ ├── ColorSelector.tsx │ │ ├── ExpandableBloc.tsx │ │ ├── FileDropZone.tsx │ │ ├── OptionSelector.tsx │ │ └── SelectAutocomplete.tsx │ ├── shared │ │ ├── motions │ │ │ ├── EaseOut.tsx │ │ │ ├── TapScale.tsx │ │ │ └── types.ts │ │ └── theme.tsx │ ├── side-views │ │ ├── CurrentNodeView.tsx │ │ └── JSONView.tsx │ ├── tools │ │ └── Fallback.tsx │ └── tour │ │ └── AppTour.tsx ├── config │ └── config.ts ├── hooks │ ├── useFlowSocketListeners.tsx │ ├── useFormFields.tsx │ ├── useHandlePositions.tsx │ ├── useHandleShowOutput.tsx │ ├── useIsPlaying.tsx │ ├── useIsTouchDevice.tsx │ ├── useLoading.tsx │ ├── useLocalStorage.tsx │ └── useRefreshOnAppearanceChange.tsx ├── i18n.js ├── index.css ├── index.tsx ├── init.js ├── layout │ └── main-layout │ │ ├── AppLayout.tsx │ │ ├── header │ │ ├── Tab.tsx │ │ └── TabHeader.tsx │ │ └── wrapper │ │ ├── FlowErrorBoundary.tsx │ │ └── FlowWrapper.tsx ├── nodes-configuration │ ├── dallENode.ts │ ├── data │ │ └── aiAction.ts │ ├── gptVisionNode.ts │ ├── inputTextNode.ts │ ├── llmPrompt.ts │ ├── mergerPromptNode.ts │ ├── nodeConfig.ts │ ├── sectionConfig.ts │ ├── stableDiffusionStabilityAiNode.ts │ ├── types.ts │ ├── urlNode.ts │ └── youtubeTranscriptNode.ts ├── providers │ ├── FlowDataProvider.tsx │ ├── NodeProvider.tsx │ ├── SocketProvider.tsx │ ├── ThemeProvider.tsx │ └── VisibilityProvider.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── services │ └── tabStorage.ts ├── setupTests.ts ├── sockets │ ├── flowEventTypes.ts │ └── flowSocket.ts ├── utils │ ├── evaluateConditions.ts │ ├── flowChecker.ts │ ├── flowUtils.ts │ ├── mappings.tsx │ ├── navigatorUtils.ts │ ├── nodeConfigurationUtils.ts │ ├── nodeUtils.ts │ ├── openAPIUtils.ts │ └── toastUtils.tsx └── vite-env.d.ts ├── tailwind.config.js ├── test ├── e2e │ ├── intro-flow.spec.ts │ ├── loading-screen.spec.ts │ ├── main-content.spec.ts │ ├── sidebar-default-nodes.spec.ts │ ├── sidebar-extensions-nodes.spec.ts │ └── tuto-display.spec.ts ├── unit │ ├── flowChecker.test.ts │ └── flowUtils.test.ts └── utils.ts ├── tsconfig.json ├── vite.config.ts └── vitest.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [DahnM20] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Docker Compose Build | Healthcheck | Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | - develop-features-0.8.1 9 | 10 | jobs: 11 | build_and_test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Move to docker directory and run docker compose 19 | run: | 20 | cd docker 21 | docker compose -f docker-compose.it.yml up -d 22 | 23 | - name: Run healthcheck script 24 | run: | 25 | cd docker 26 | chmod +x healthcheck.sh 27 | ./healthcheck.sh http://localhost:5000/healthcheck 28 | 29 | - name: Print Docker logs 30 | if: failure() 31 | run: | 32 | cd docker 33 | docker compose logs 34 | 35 | - name: Run UI unit tests 36 | run: | 37 | cd packages/ui 38 | npm i 39 | npm run test 40 | 41 | - name: Run Python unit tests 42 | run: | 43 | docker exec ai-flow-backend python -m unittest discover -s tests/unit -p '*test_*.py' 44 | 45 | - name: Run integration tests 46 | run: | 47 | cd integration_tests 48 | npm i 49 | npm run test 50 | 51 | - name: Print Docker logs 52 | if: failure() 53 | run: | 54 | cd docker 55 | docker compose logs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/backend/.env 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dahn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/ai-flow-pro-homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/ai-flow-pro-homepage.png -------------------------------------------------------------------------------- /assets/comparison-pro-vs-opensource-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/comparison-pro-vs-opensource-v2.png -------------------------------------------------------------------------------- /assets/flow-example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/flow-example-2.png -------------------------------------------------------------------------------- /assets/flow-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/flow-example.png -------------------------------------------------------------------------------- /assets/gpt-vision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/gpt-vision.png -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/header.png -------------------------------------------------------------------------------- /assets/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/intro.png -------------------------------------------------------------------------------- /assets/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/layout.png -------------------------------------------------------------------------------- /assets/predefined-prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/predefined-prompts.png -------------------------------------------------------------------------------- /assets/presentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/presentation.png -------------------------------------------------------------------------------- /assets/replicate-models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/replicate-models.png -------------------------------------------------------------------------------- /assets/scenario-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/scenario-1-1.png -------------------------------------------------------------------------------- /assets/scenario-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/scenario-1-2.png -------------------------------------------------------------------------------- /assets/scenario-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/scenario-example.png -------------------------------------------------------------------------------- /assets/split-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/split-input.png -------------------------------------------------------------------------------- /assets/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/assets/summary.png -------------------------------------------------------------------------------- /bin/generate_python_classes_from_ts.sh: -------------------------------------------------------------------------------- 1 | npm i -g typescript-json-schema 2 | typescript-json-schema "../packages/ui/src/nodes-configuration/types.ts" "*" --out "schema.json" 3 | mv schema.json ../packages/backend/app/processors/components/ 4 | cd ../packages/backend/app/processors/components/ 5 | poetry run datamodel-codegen --input schema.json --input-file-type jsonschema --output model.py --output-model-type pydantic_v2.BaseModel --enum-field-as-literal all 6 | rm schema.json 7 | echo "model.py generated" -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## 🐳 Docker 2 | 3 | ### Docker Compose 4 | 5 | 1. Go to the docker directory: `cd ./docker` 6 | 2. Update the .yml if needed for the PORTS 7 | 3. Launch `docker-compose up` or `docker-compose up -d` 8 | 4. Open your browser and navigate to `http://localhost:3000` 9 | 5. Use `docker-compose stop` when you want to stop the app. -------------------------------------------------------------------------------- /docker/docker-compose.it.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | container_name: ai-flow-backend 4 | build: 5 | context: ../packages/backend/ 6 | dockerfile: Dockerfile 7 | ports: 8 | - 5000:5000 9 | environment: 10 | - HOST=0.0.0.0 11 | - PORT=5000 12 | - DEPLOYMENT_ENV=LOCAL 13 | - LOCAL_STORAGE_FOLDER_NAME=local_storage 14 | - USE_MOCK=true 15 | volumes: 16 | - ./ai-flow-backend-storage:/app/local_storage 17 | 18 | frontend: 19 | container_name: ai-flow-frontend 20 | build: 21 | context: ../packages/ui/ 22 | dockerfile: Dockerfile 23 | ports: 24 | - 80:80 25 | environment: 26 | - VITE_APP_WS_HOST=localhost 27 | - VITE_APP_WS_PORT=5000 28 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | container_name: ai-flow-backend 4 | build: 5 | context: ../packages/backend/ 6 | dockerfile: Dockerfile 7 | ports: 8 | - 5001:5000 9 | environment: 10 | - HOST=0.0.0.0 11 | - PORT=5000 12 | - DEPLOYMENT_ENV=LOCAL 13 | - REPLICATE_API_KEY=sample 14 | - LOCAL_STORAGE_FOLDER_NAME=local_storage 15 | volumes: 16 | - ./ai-flow-backend-storage:/app/local_storage 17 | 18 | frontend: 19 | container_name: ai-flow-frontend 20 | build: 21 | context: ../packages/ui/ 22 | dockerfile: Dockerfile 23 | args: 24 | VITE_APP_WS_HOST: localhost 25 | VITE_APP_WS_PORT: 5001 26 | VITE_APP_API_REST_PORT: 5001 27 | ports: 28 | - 80:80 29 | -------------------------------------------------------------------------------- /docker/healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | URL="$1" 9 | INTERVAL=5 10 | MAX_ATTEMPTS=20 11 | 12 | attempt=0 13 | while [ $attempt -lt $MAX_ATTEMPTS ]; do 14 | attempt=$(( $attempt + 1 )) 15 | 16 | curl --fail --silent $URL && echo "Service is up!" && exit 0 17 | 18 | echo "Service not ready yet. Waiting for $INTERVAL seconds. Attempt $attempt of $MAX_ATTEMPTS." 19 | sleep $INTERVAL 20 | done 21 | 22 | echo "Service did not become ready after $MAX_ATTEMPTS attempts." 23 | exit 1 -------------------------------------------------------------------------------- /integration_tests/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /integration_tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "mocha dist/tests/**/*Test.js", 8 | "build": "tsc", 9 | "pretest": "npm run build" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^1.5.0", 15 | "chai": "^4.3.8", 16 | "mocha": "^10.2.0", 17 | "socket.io-client": "^4.7.2" 18 | }, 19 | "devDependencies": { 20 | "@types/chai": "^4.3.6", 21 | "@types/minimist": "^1.2.2", 22 | "@types/mocha": "^10.0.1", 23 | "@types/node": "^20.5.9", 24 | "@types/normalize-package-data": "^2.4.1", 25 | "@types/socket.io-client": "^3.0.0", 26 | "typescript": "^5.2.2" 27 | } 28 | } -------------------------------------------------------------------------------- /integration_tests/tests/nodeProcessingOrder/nodeParallelExecutionDurationTest.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | disconnectSocket, 4 | getSocket, 5 | setupSocket, 6 | } from "../../utils/testHooks"; 7 | import { 8 | createRequestData, 9 | flowWithFourParallelNodeStep, 10 | } from "../../utils/requestDatas"; 11 | 12 | describe("Node errors test", function () { 13 | this.timeout(15000); 14 | 15 | beforeEach(function (done) { 16 | setupSocket(done); 17 | }); 18 | 19 | afterEach(function () { 20 | disconnectSocket(); 21 | }); 22 | 23 | it("4 parallel node with 2s sleep each should not compound time", function (done) { 24 | const socket = getSocket(); 25 | 26 | const flow = structuredClone(flowWithFourParallelNodeStep); 27 | 28 | flow[1] = { 29 | ...flow[1], 30 | sleepDuration: 2, 31 | }; 32 | 33 | flow[2] = { 34 | ...flow[2], 35 | sleepDuration: 2, 36 | }; 37 | 38 | flow[3] = { 39 | ...flow[3], 40 | sleepDuration: 2, 41 | }; 42 | 43 | flow[4] = { 44 | ...flow[4], 45 | sleepDuration: 2, 46 | }; 47 | 48 | const maxDurationMsExpected = 5000; 49 | const timeStart = Date.now(); 50 | socket.emit("process_file", createRequestData(flow)); 51 | 52 | let progressCount = 0; 53 | const maxProgress = 5; 54 | 55 | socket.on("progress", (progress) => { 56 | progressCount++; 57 | if (progressCount > maxProgress) { 58 | done(new Error(`Too many nodes sent progress`)); 59 | } 60 | }); 61 | 62 | socket.on("run_end", (end) => { 63 | if (progressCount !== maxProgress) { 64 | done( 65 | new Error(`Not all nodes were processed before end of execution.`) 66 | ); 67 | } else { 68 | const timeEnd = Date.now(); 69 | const duration = timeEnd - timeStart; 70 | expect(duration).to.be.lessThan(maxDurationMsExpected); 71 | done(); 72 | } 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /integration_tests/tests/nodeProcessingOrder/nodesWithoutLinkTest.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { disconnectSocket, getSocket, setupSocket } from "../../utils/testHooks"; 3 | import { createRequestData } from "../../utils/requestDatas"; 4 | 5 | describe('node without link test', function () { 6 | this.timeout(15000); 7 | 8 | beforeEach(function (done) { 9 | setupSocket(done); 10 | }); 11 | 12 | afterEach(function () { 13 | disconnectSocket(); 14 | }); 15 | 16 | const flowWithNodesWithoutLink = [ 17 | { 18 | inputs: [], 19 | name: "1#llm-prompt", 20 | processorType: "llm-prompt", 21 | }, 22 | { 23 | inputs: [], 24 | name: "2#llm-prompt", 25 | processorType: "llm-prompt", 26 | }, 27 | { 28 | inputs: [], 29 | name: "3#stable-diffusion-stabilityai-prompt", 30 | processorType: "stable-diffusion-stabilityai-prompt", 31 | } 32 | ]; 33 | 34 | it('process_file should process all the nodes', function (done) { 35 | const socket = getSocket(); 36 | socket.emit('process_file', createRequestData(flowWithNodesWithoutLink)); 37 | 38 | let processedNodes: string[] = []; 39 | 40 | socket.on('progress', (data) => { 41 | processedNodes.push(data.instanceName); 42 | 43 | if (processedNodes.length === flowWithNodesWithoutLink.length) { 44 | try { 45 | expect(processedNodes).to.includes(flowWithNodesWithoutLink[0].name); 46 | expect(processedNodes).to.includes(flowWithNodesWithoutLink[1].name); 47 | expect(processedNodes).to.includes(flowWithNodesWithoutLink[2].name); 48 | done(); 49 | } catch (error) { 50 | done(error); 51 | } 52 | } 53 | }); 54 | 55 | socket.on('error', (error) => { 56 | done(new Error(`Error event received: ${JSON.stringify(error)}`)); 57 | }); 58 | }); 59 | 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /integration_tests/tests/nodeProcessingOrder/singleNodeTest.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Socket, io } from "socket.io-client"; 3 | import { createRequestData } from "../../utils/requestDatas"; 4 | 5 | describe('single node test', function () { 6 | this.timeout(5000); 7 | 8 | let socket: Socket; 9 | 10 | beforeEach(function (done) { 11 | socket = io('http://localhost:5000'); 12 | 13 | socket.on('connect', function () { 14 | done(); 15 | }); 16 | 17 | socket.on('connect_error', function (error) { 18 | done(error); 19 | }); 20 | }); 21 | 22 | afterEach(function () { 23 | socket.disconnect(); 24 | }); 25 | 26 | const flowWithSingleNode = [ 27 | { 28 | inputs: [], 29 | name: "1#llm-prompt", 30 | processorType: "llm-prompt", 31 | } 32 | ]; 33 | 34 | it('process_file should trigger one progress event', function (done) { 35 | socket.emit('process_file', createRequestData(flowWithSingleNode)); 36 | 37 | socket.once('progress', (data) => { 38 | expect(data).to.have.property('instanceName').to.equal(flowWithSingleNode[0].name); 39 | done(); 40 | }); 41 | 42 | socket.once('error', (error) => { 43 | done(new Error(`Error event received: ${JSON.stringify(error)}`)); 44 | }); 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /integration_tests/tests/socketEvents/socketConnectionTest.ts: -------------------------------------------------------------------------------- 1 | import { io, Socket } from "socket.io-client"; 2 | import { expect } from 'chai'; 3 | 4 | describe('Socket.IO connection tests', function () { 5 | 6 | let socket: Socket; 7 | 8 | beforeEach(function (done: Mocha.Done): void { 9 | socket = io('http://localhost:5000'); 10 | 11 | socket.on('connect', function (): void { 12 | done(); 13 | }); 14 | 15 | socket.on('connect_error', function (error: any): void { 16 | done(error); 17 | }); 18 | }); 19 | 20 | afterEach(function (): void { 21 | socket.disconnect(); 22 | }); 23 | 24 | it('should be connected to the server', function (done: Mocha.Done): void { 25 | expect(socket.connected).to.be.true; 26 | done(); 27 | }); 28 | 29 | it('should disconnect', function (done: Mocha.Done): void { 30 | socket.disconnect(); 31 | expect(socket.connected).to.be.false; 32 | done(); 33 | }); 34 | }); -------------------------------------------------------------------------------- /integration_tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./", 7 | "strict": true 8 | } 9 | } -------------------------------------------------------------------------------- /integration_tests/utils/testHooks.ts: -------------------------------------------------------------------------------- 1 | import { Socket, io } from "socket.io-client"; 2 | 3 | let socket: Socket; 4 | 5 | export const setupSocket = (done: any) => { 6 | socket = io('http://localhost:5000'); 7 | 8 | socket.on('connect', function () { 9 | done(); 10 | }); 11 | 12 | socket.on('connect_error', function (error) { 13 | done(error); 14 | }); 15 | }; 16 | 17 | export const disconnectSocket = () => { 18 | socket.disconnect(); 19 | }; 20 | 21 | export const getSocket = () => socket; 22 | -------------------------------------------------------------------------------- /packages/backend/.env: -------------------------------------------------------------------------------- 1 | # Sample .env - do not commit yours 2 | 3 | HOST=0.0.0.0 4 | PORT=5000 5 | DEPLOYMENT_ENV=LOCAL 6 | 7 | # AWS 8 | S3_BUCKET_NAME= 9 | AWS_ACCESS_KEY_ID= 10 | AWS_SECRET_ACCESS_KEY= 11 | AWS_REGION_NAME= 12 | 13 | USE_ENV_API_KEYS=false 14 | OPENAI_API_KEY= 15 | STABILITYAI_API_KEY= 16 | REPLICATE_API_KEY= # Used to fetch models and collections info 17 | SERVE_STATIC_FILES=true 18 | LOCAL_STORAGE_FOLDER_NAME=local_storage 19 | STABLE_DIFFUSION_STABILITYAI_API_HOST=https://api.stability.ai 20 | REPLICATE_MODELS_HIGHLIGHTED=stability-ai/stable-video-diffusion,mistralai/mixtral-8x7b-instruct-v0.1,batouresearch/high-resolution-controlnet-tile,meta/musicgen 21 | -------------------------------------------------------------------------------- /packages/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Fichiers générés par l'environnement de développement 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Fichiers générés par l'IDE 6 | .idea/ 7 | .vscode/ 8 | 9 | # Fichiers de logs 10 | *.log 11 | 12 | # Fichiers d'env 13 | *.env 14 | 15 | # Fichiers de build 16 | build 17 | dist 18 | server.spec 19 | 20 | # Local storage 21 | local_storage/ -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | # Default values 4 | ENV HOST=0.0.0.0 5 | ENV PORT=5000 6 | 7 | 8 | WORKDIR /app 9 | 10 | # System dependencies 11 | RUN apt-get update && apt-get install -y \ 12 | build-essential \ 13 | libpq-dev \ 14 | python3-dev \ 15 | libssl-dev \ 16 | libffi-dev \ 17 | libmagic-dev \ 18 | && apt-get clean \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | 22 | # Playwright 23 | ARG PLAYWRIGHT_VERSION=1.39 24 | ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright 25 | 26 | RUN pip install playwright==$PLAYWRIGHT_VERSION && \ 27 | playwright install chromium && \ 28 | playwright install-deps chromium 29 | 30 | # Poetry & Dependencies 31 | RUN pip install --upgrade poetry \ 32 | && poetry config virtualenvs.create false 33 | 34 | COPY poetry.lock pyproject.toml /app/ 35 | 36 | RUN poetry install --no-interaction --no-root 37 | 38 | # The rest of the app 39 | COPY app /app/app/ 40 | COPY templates /app/templates 41 | COPY resources /app/resources 42 | COPY tests/ /app/tests/ 43 | COPY server.py README.md /app/ 44 | COPY config.yaml /app/ 45 | 46 | EXPOSE 5000 47 | 48 | CMD ["poetry", "run", "python", "server.py"] -------------------------------------------------------------------------------- /packages/backend/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/backend/README.md -------------------------------------------------------------------------------- /packages/backend/app/flask/app_routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/backend/app/flask/app_routes/__init__.py -------------------------------------------------------------------------------- /packages/backend/app/flask/app_routes/image_routes.py: -------------------------------------------------------------------------------- 1 | from app.env_config import (get_local_storage_folder_path) 2 | from flask import Blueprint, send_from_directory 3 | 4 | image_blueprint = Blueprint('image_blueprint', __name__) 5 | 6 | @image_blueprint.route("/image/") 7 | def serve_image(filename): 8 | """ 9 | Serve image from local storage. 10 | """ 11 | return send_from_directory(get_local_storage_folder_path(), filename) -------------------------------------------------------------------------------- /packages/backend/app/flask/app_routes/parameters_routes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | from flask import Blueprint 4 | 5 | parameters_blueprint = Blueprint("parameters_blueprint", __name__) 6 | 7 | 8 | def load_config(): 9 | with open("config.yaml", "r") as file: 10 | return yaml.safe_load(file) 11 | 12 | 13 | @parameters_blueprint.route("/parameters", methods=["GET"]) 14 | def parameters(): 15 | config = load_config() 16 | return config 17 | -------------------------------------------------------------------------------- /packages/backend/app/flask/app_routes/static_routes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Blueprint, send_from_directory 3 | 4 | from ...env_config import get_static_folder 5 | 6 | static_blueprint = Blueprint('static_blueprint', __name__) 7 | 8 | 9 | @static_blueprint.route("/", defaults={"path": ""}) 10 | @static_blueprint.route("/") 11 | def serve(path): 12 | """ 13 | Serve UI static files from the static folder. 14 | """ 15 | static_folder = get_static_folder() 16 | if path != "" and os.path.exists(os.path.join(static_folder, path)): 17 | return send_from_directory(static_folder, path) 18 | else: 19 | return send_from_directory(static_folder, "index.html") -------------------------------------------------------------------------------- /packages/backend/app/flask/app_routes/template_routes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from flask import Blueprint, request 3 | from ...providers.template.template_provider import TemplateProvider 4 | from ...root_injector import get_root_injector 5 | 6 | template_blueprint = Blueprint("template_blueprint", __name__) 7 | 8 | 9 | @template_blueprint.route("/template") 10 | def get_default_templates(): 11 | cursor = request.args.get("cursor", None) 12 | 13 | template_provider = get_root_injector().get(TemplateProvider) 14 | 15 | templates = template_provider.get_templates(cursor=cursor) 16 | 17 | return templates 18 | 19 | 20 | @template_blueprint.route("/template/") 21 | def get_template_by_id(template_id): 22 | template_provider = get_root_injector().get(TemplateProvider) 23 | 24 | template = template_provider.get_template_by_id(template_id) 25 | 26 | return template 27 | 28 | 29 | @template_blueprint.route("/template", methods=["POST"]) 30 | def save_template(): 31 | template_data = request.json 32 | template_provider = get_root_injector().get(TemplateProvider) 33 | 34 | template_provider.save_template(template_data) 35 | return "Template saved successfully." 36 | -------------------------------------------------------------------------------- /packages/backend/app/flask/app_routes/upload_routes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from flask import Blueprint 3 | from ...storage.storage_strategy import StorageStrategy 4 | 5 | from ...root_injector import get_root_injector 6 | from flask import request 7 | 8 | upload_blueprint = Blueprint("upload_blueprint", __name__) 9 | 10 | 11 | @upload_blueprint.route("/upload") 12 | def upload_file(): 13 | """ 14 | Serve image from local storage. 15 | """ 16 | 17 | logging.info("Uploading file") 18 | storage_strategy = get_root_injector().get(StorageStrategy) 19 | 20 | filename = request.args.get("filename") 21 | try: 22 | data = storage_strategy.get_upload_link(filename) 23 | except Exception as e: 24 | logging.error(e) 25 | raise Exception( 26 | "Error uploading file. " 27 | "Please check your S3 configuration. " 28 | "If you've not configured S3 please refer to docs.ai-flow.net/docs/file-upload" 29 | ) 30 | 31 | json_link = { 32 | "upload_data": data[0], 33 | "download_link": data[1], 34 | } 35 | 36 | return json_link 37 | -------------------------------------------------------------------------------- /packages/backend/app/flask/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import jsonify, request, g 4 | from flask_socketio import emit 5 | import json 6 | 7 | 8 | def with_flow_data_validations(*validation_funcs): 9 | def decorator(func): 10 | @wraps(func) 11 | def wrapper(data, *args, **kwargs): 12 | try: 13 | flow_data = json.loads(data.get("jsonFile", "{}")) 14 | 15 | for validation_func in validation_funcs: 16 | validation_func(flow_data) 17 | 18 | return func(data, *args, **kwargs) 19 | except Exception as e: 20 | emit("error", {"error": str(e)}) 21 | 22 | return wrapper 23 | 24 | return decorator 25 | -------------------------------------------------------------------------------- /packages/backend/app/flask/flask_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from flask import Flask, request, redirect 3 | from flask_cors import CORS 4 | import os 5 | 6 | from ..env_config import get_flask_secret_key, get_static_folder 7 | 8 | def create_app(): 9 | app = Flask(__name__, static_folder=get_static_folder()) 10 | 11 | if get_flask_secret_key() is not None : 12 | logging.info("Flask secret key set") 13 | app.config['SECRET_KEY'] = get_flask_secret_key() 14 | else : 15 | logging.warning("Flask secret key not set") 16 | app.config['SECRET_KEY'] = "default_secret" 17 | 18 | CORS(app) 19 | 20 | 21 | if os.getenv("USE_HTTPS", "false").lower() == "true": 22 | 23 | @app.before_request 24 | def before_request(): 25 | if not request.is_secure: 26 | url = request.url.replace("http://", "https://", 1) 27 | return redirect(url, code=301) 28 | 29 | logging.info("App created") 30 | return app 31 | -------------------------------------------------------------------------------- /packages/backend/app/flask/routes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.env_config import is_server_static_files_enabled, is_local_environment 3 | from app.flask.socketio_init import flask_app 4 | from .utils.constants import HTTP_OK 5 | 6 | 7 | @flask_app.route("/healthcheck", methods=["GET"]) 8 | def healthcheck(): 9 | return "OK", HTTP_OK 10 | 11 | 12 | from .app_routes.node_routes import node_blueprint 13 | from .app_routes.template_routes import template_blueprint 14 | 15 | flask_app.register_blueprint(node_blueprint) 16 | flask_app.register_blueprint(template_blueprint) 17 | 18 | from .app_routes.upload_routes import upload_blueprint 19 | 20 | flask_app.register_blueprint(upload_blueprint) 21 | 22 | from .app_routes.parameters_routes import parameters_blueprint 23 | 24 | flask_app.register_blueprint(parameters_blueprint) 25 | 26 | if is_server_static_files_enabled(): 27 | from .app_routes.static_routes import static_blueprint 28 | 29 | logging.info("Visual interface will be available at http://localhost:5000") 30 | flask_app.register_blueprint(static_blueprint) 31 | 32 | if is_local_environment(): 33 | from .app_routes.image_routes import image_blueprint 34 | 35 | logging.info("Environment set to LOCAL") 36 | flask_app.register_blueprint(image_blueprint) 37 | -------------------------------------------------------------------------------- /packages/backend/app/flask/socketio_init.py: -------------------------------------------------------------------------------- 1 | import eventlet 2 | 3 | eventlet.monkey_patch(all=False, socket=True) 4 | 5 | from flask_socketio import SocketIO 6 | from .flask_app import create_app 7 | 8 | flask_app = create_app() 9 | socketio = SocketIO(flask_app, cors_allowed_origins="*", async_mode="eventlet") -------------------------------------------------------------------------------- /packages/backend/app/flask/utils/constants.py: -------------------------------------------------------------------------------- 1 | HTTP_OK = 200 2 | HTTP_BAD_REQUEST = 400 3 | HTTP_NOT_FOUND = 404 4 | HTTP_UNAUTHORIZED = 401 5 | 6 | 7 | SESSION_USER_ID_KEY = "user_id" 8 | 9 | PARAMETERS_FIELD_NAME = "parameters" 10 | 11 | ENV_API_KEYS = [ 12 | "openai_api_key", 13 | "stabilityai_api_key", 14 | "replicate_api_key", 15 | "anthropic_api_key", 16 | "openrouter_api_key", 17 | ] 18 | -------------------------------------------------------------------------------- /packages/backend/app/flask/validators.py: -------------------------------------------------------------------------------- 1 | from ..env_config import is_cloud_env 2 | 3 | MAX_NODES_IN_LIVE_DEMO = 20 4 | MAX_URL_INPUT_NODE_IN_LIVE_DEMO = 5 5 | 6 | 7 | def max_nodes(flow_data): 8 | if not is_cloud_env(): 9 | return 10 | 11 | if len(flow_data) > MAX_NODES_IN_LIVE_DEMO: 12 | raise Exception( 13 | f"You've created too many nodes for this flow (>{MAX_NODES_IN_LIVE_DEMO})." 14 | ) 15 | 16 | 17 | def max_url_input_nodes(flow_data): 18 | if not is_cloud_env(): 19 | return 20 | 21 | count_url_input = sum( 22 | 1 for node in flow_data if node.get("processorType") == "url_input" 23 | ) 24 | 25 | if count_url_input > MAX_URL_INPUT_NODE_IN_LIVE_DEMO: 26 | raise Exception( 27 | f"You cannot have more than {MAX_URL_INPUT_NODE_IN_LIVE_DEMO} web extractor nodes in this flow. Please, create another flow." 28 | ) 29 | 30 | 31 | def max_empty_output_data(flow_data): 32 | if not is_cloud_env(): 33 | return 34 | 35 | count_empty_output_data = sum(1 for node in flow_data if not node.get("outputData")) 36 | 37 | if count_empty_output_data > MAX_NODES_IN_LIVE_DEMO: 38 | raise Exception( 39 | f"You've created too many nodes for the live demo (>{MAX_NODES_IN_LIVE_DEMO}). If you want to not be limited, please install the tool locally." 40 | ) 41 | -------------------------------------------------------------------------------- /packages/backend/app/llms/factory/llm_factory.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from llama_index.core.base.llms.base import BaseLLM 4 | 5 | 6 | class LLMFactory(ABC): 7 | @abstractmethod 8 | def create_llm(self, model: str, **kwargs) -> BaseLLM: 9 | pass 10 | -------------------------------------------------------------------------------- /packages/backend/app/llms/factory/paid_api_llm_factory.py: -------------------------------------------------------------------------------- 1 | from .llm_factory import LLMFactory 2 | from injector import singleton 3 | 4 | 5 | from llama_index.core.base.llms.base import BaseLLM 6 | 7 | 8 | @singleton 9 | class PaidAPILLMFactory(LLMFactory): 10 | API_KEY_FIELD = "api_key" 11 | 12 | def create_llm(self, model: str, **kwargs) -> BaseLLM: 13 | if "gpt" in model: 14 | from llama_index.llms.openai import OpenAI 15 | 16 | return OpenAI( 17 | model=model, api_key=kwargs.get(PaidAPILLMFactory.API_KEY_FIELD) 18 | ) 19 | else: 20 | raise ValueError(f"Unknown model {model}") 21 | -------------------------------------------------------------------------------- /packages/backend/app/llms/prompt_engine/prompt_engine.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class PromptEngine(ABC): 5 | @abstractmethod 6 | def prompt(self, messages): 7 | pass 8 | 9 | @abstractmethod 10 | def prompt_stream(self, messages): 11 | pass 12 | -------------------------------------------------------------------------------- /packages/backend/app/llms/prompt_engine/simple_prompt_engine.py: -------------------------------------------------------------------------------- 1 | from ..factory.llm_factory import LLMFactory 2 | from .prompt_engine import PromptEngine 3 | from ...root_injector import get_root_injector 4 | 5 | 6 | class SimplePromptEngine(PromptEngine): 7 | llm_factory: LLMFactory 8 | 9 | def __init__(self, model, api_key, custom_llm_factory=None): 10 | self.model = model 11 | self.api_key = api_key 12 | if custom_llm_factory is None: 13 | custom_llm_factory = self._get_default_llm_factory() 14 | 15 | self.llm_factory = custom_llm_factory 16 | 17 | @staticmethod 18 | def _get_default_llm_factory(): 19 | return get_root_injector().get(LLMFactory) 20 | 21 | def prompt(self, messages): 22 | llm = self.llm_factory.create_llm(self.model, api_key=self.api_key) 23 | chat_response = llm.chat(messages) 24 | return chat_response 25 | 26 | def prompt_stream(self, messages): 27 | llm = self.llm_factory.create_llm(self.model, api_key=self.api_key) 28 | chat_response = llm.stream_chat(messages) 29 | return chat_response 30 | -------------------------------------------------------------------------------- /packages/backend/app/llms/utils/max_token_for_model.py: -------------------------------------------------------------------------------- 1 | import tiktoken 2 | 3 | DEFAULT_MAX_TOKEN = 4097 4 | 5 | 6 | def max_token_for_model(model_name: str) -> int: 7 | if "gpt-4o" in model_name: 8 | return 128000 9 | token_data = { 10 | # GPT-4.1 models 11 | "gpt-4.1": 1047576, 12 | "gpt-4.1-mini": 1047576, 13 | "gpt-4.1-nano": 1047576, 14 | # GPT-4 models 15 | "gpt-4o": 128000, 16 | "gpt-4o-2024-11-20": 128000, 17 | "gpt-4o-mini": 128000, 18 | "gpt-4-turbo": 128000, 19 | "gpt-4-turbo-preview": 128000, 20 | "gpt-4-1106-preview": 128000, 21 | "gpt-4-vision-preview": 128000, 22 | "gpt-4": 8192, 23 | "gpt-4-0613": 8192, 24 | "gpt-4-32k": 32768, 25 | "gpt-4-32k-0613": 32768, 26 | "gpt-4-0314": 8192, 27 | "gpt-4-32k-0314": 32768, 28 | # GPT-3.5 models 29 | "gpt-3.5-turbo": 16385, 30 | "gpt-3.5-turbo-1106": 16385, 31 | "gpt-3.5-turbo-16k": 16385, 32 | "gpt-3.5-turbo-instruct": 4097, 33 | "gpt-3.5-turbo-0613": 4097, 34 | "gpt-3.5-turbo-16k-0613": 16385, 35 | "gpt-3.5-turbo-0301": 4097, 36 | # Other GPT-3.5 models 37 | "text-davinci-003": 4097, 38 | "text-davinci-002": 4097, 39 | "code-davinci-002": 8001, 40 | } 41 | return token_data.get(model_name, DEFAULT_MAX_TOKEN) 42 | 43 | 44 | def nb_token_for_input(input: str, model_name: str) -> int: 45 | try: 46 | return len(tiktoken.encoding_for_model(model_name).encode(input)) 47 | except Exception as e: 48 | default_model_for_token = "gpt-4o" 49 | return len(tiktoken.encoding_for_model(default_model_for_token).encode(input)) 50 | -------------------------------------------------------------------------------- /packages/backend/app/log_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import colorlog 3 | 4 | 5 | def setup_logger(name: str): 6 | formatter = colorlog.ColoredFormatter( 7 | "%(log_color)s%(levelname)-8s%(reset)s %(message)s", 8 | datefmt=None, 9 | reset=True, 10 | log_colors={ 11 | "DEBUG": "cyan", 12 | "INFO": "green", 13 | "WARNING": "yellow", 14 | "ERROR": "red", 15 | "CRITICAL": "red", 16 | }, 17 | ) 18 | 19 | logger = logging.getLogger(name) 20 | handler = logging.StreamHandler() 21 | handler.setFormatter(formatter) 22 | logger.addHandler(handler) 23 | 24 | return logger 25 | 26 | 27 | root_logger = setup_logger("root") 28 | root_logger.setLevel(logging.INFO) 29 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/backend/app/processors/components/__init__.py -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/backend/app/processors/components/core/__init__.py -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/dall_e_prompt_processor.py: -------------------------------------------------------------------------------- 1 | from ...context.processor_context import ProcessorContext 2 | from ..processor import ContextAwareProcessor 3 | 4 | from openai import OpenAI 5 | 6 | from .processor_type_name_utils import ProcessorType 7 | 8 | 9 | class DallEPromptProcessor(ContextAwareProcessor): 10 | processor_type = ProcessorType.DALLE_PROMPT 11 | 12 | DEFAULT_MODEL = "dall-e-3" 13 | DEFAULT_SIZE = "1024x1024" 14 | DEFAULT_QUALITY = "standard" 15 | 16 | def __init__(self, config, context: ProcessorContext): 17 | super().__init__(config, context) 18 | self.prompt = config.get("prompt") 19 | self.size = config.get("size", DallEPromptProcessor.DEFAULT_SIZE) 20 | self.quality = config.get("quality", DallEPromptProcessor.DEFAULT_QUALITY) 21 | 22 | def process(self): 23 | if self.get_input_processor() is not None: 24 | self.prompt = ( 25 | self.get_input_processor().get_output(self.get_input_node_output_key()) 26 | if self.prompt is None or len(self.prompt) == 0 27 | else self.prompt 28 | ) 29 | 30 | api_key = self._processor_context.get_value("openai_api_key") 31 | client = OpenAI( 32 | api_key=api_key, 33 | ) 34 | 35 | response = client.images.generate( 36 | model=DallEPromptProcessor.DEFAULT_MODEL, 37 | prompt=self.prompt, 38 | n=1, 39 | size=self.size, 40 | quality=self.quality, 41 | ) 42 | 43 | return response.data[0].url 44 | 45 | def cancel(self): 46 | pass 47 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/display_processor.py: -------------------------------------------------------------------------------- 1 | from .processor_type_name_utils import ProcessorType 2 | from ..processor import BasicProcessor 3 | 4 | 5 | class DisplayProcessor(BasicProcessor): 6 | processor_type = "display" 7 | 8 | def __init__(self, config): 9 | super().__init__(config) 10 | 11 | def process(self): 12 | input_data = None 13 | if self.get_input_processor() is None: 14 | return "" 15 | 16 | input_data = self.get_input_processor().get_output( 17 | self.get_input_node_output_key() 18 | ) 19 | 20 | return input_data 21 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/file_processor.py: -------------------------------------------------------------------------------- 1 | from .processor_type_name_utils import ProcessorType 2 | from ..processor import BasicProcessor 3 | 4 | 5 | class FileProcessor(BasicProcessor): 6 | processor_type = ProcessorType.FILE 7 | 8 | def __init__(self, config): 9 | super().__init__(config) 10 | self.url = config["fileUrl"] 11 | 12 | def process(self): 13 | return self.url 14 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/input_image_processor.py: -------------------------------------------------------------------------------- 1 | from .processor_type_name_utils import ProcessorType 2 | from ..processor import BasicProcessor 3 | 4 | 5 | class InputImageProcessor(BasicProcessor): 6 | processor_type = ProcessorType.INPUT_IMAGE 7 | 8 | def __init__(self, config): 9 | super().__init__(config) 10 | self.inputText = config["inputText"] 11 | 12 | def process(self): 13 | return self.inputText 14 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/input_processor.py: -------------------------------------------------------------------------------- 1 | from .processor_type_name_utils import ProcessorType 2 | from ..processor import BasicProcessor 3 | 4 | 5 | class InputProcessor(BasicProcessor): 6 | processor_type = ProcessorType.INPUT_TEXT 7 | 8 | def __init__(self, config): 9 | super().__init__(config) 10 | self.inputText = config["inputText"] 11 | 12 | def process(self): 13 | return self.inputText 14 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/merge_processor.py: -------------------------------------------------------------------------------- 1 | from ..processor import ContextAwareProcessor 2 | from .processor_type_name_utils import ProcessorType, MergeModeEnum 3 | 4 | class MergeProcessor(ContextAwareProcessor): 5 | processor_type = ProcessorType.MERGER_PROMPT 6 | 7 | def __init__(self, config, context): 8 | super().__init__(config, context) 9 | 10 | self.merge_mode = MergeModeEnum(int(config["mergeMode"])) 11 | 12 | def update_prompt(self, inputs): 13 | for idx, value in enumerate(inputs, start=1): 14 | placeholder = f"${{input-{idx}}}" 15 | self.prompt = self.prompt.replace(placeholder, str(value)) 16 | 17 | def process(self): 18 | self.prompt = self.get_input_by_name("prompt", "") 19 | input_names = self.get_input_names_from_config() 20 | inputs = [self.get_input_by_name(name, "") for name in input_names] 21 | 22 | self.update_prompt(inputs) 23 | 24 | return self.prompt 25 | 26 | def cancel(self): 27 | pass 28 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/processor_type_name_utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MergeModeEnum(Enum): 5 | MERGE = 1 6 | MERGE_AND_PROMPT = 2 7 | 8 | 9 | class ProcessorType(Enum): 10 | INPUT_TEXT = "input-text" 11 | INPUT_IMAGE = "input-image" 12 | URL_INPUT = "url_input" 13 | LLM_PROMPT = "llm-prompt" 14 | GPT_VISION = "gpt-vision" 15 | YOUTUBE_TRANSCRIPT_INPUT = "youtube_transcript_input" 16 | DALLE_PROMPT = "dalle-prompt" 17 | STABLE_DIFFUSION_STABILITYAI_PROMPT = "stable-diffusion-stabilityai-prompt" 18 | STABLE_VIDEO_DIFFUSION_REPLICATE = "stable-video-diffusion-replicate" 19 | REPLICATE = "replicate" 20 | MERGER_PROMPT = "merger-prompt" 21 | AI_DATA_SPLITTER = "ai-data-splitter" 22 | TRANSITION = "transition" 23 | DISPLAY = "display" 24 | FILE = "file" 25 | STABLE_DIFFUSION_THREE = "stabilityai-stable-diffusion-3-processor" 26 | TEXT_TO_SPEECH = "openai-text-to-speech-processor" 27 | DOCUMENT_TO_TEXT = "document-to-text-processor" 28 | STABILITYAI = "stabilityai-generic-processor" 29 | CLAUDE = "claude-anthropic-processor" 30 | REPLACE_TEXT = "replace-text" 31 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/transition_processor.py: -------------------------------------------------------------------------------- 1 | from .processor_type_name_utils import ProcessorType 2 | from ..processor import BasicProcessor 3 | 4 | 5 | class TransitionProcessor(BasicProcessor): 6 | processor_type = ProcessorType.TRANSITION 7 | 8 | def __init__(self, config): 9 | super().__init__(config) 10 | 11 | def process(self): 12 | input_data = None 13 | if self.get_input_processor() is None: 14 | return "" 15 | 16 | input_data = self.get_input_processor().get_output( 17 | self.get_input_node_output_key() 18 | ) 19 | 20 | return input_data 21 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/core/youtube_transcript_input_processor.py: -------------------------------------------------------------------------------- 1 | from ..processor import BasicProcessor 2 | from langchain.document_loaders import YoutubeLoader 3 | 4 | from .processor_type_name_utils import ProcessorType 5 | 6 | 7 | class YoutubeTranscriptInputProcessor(BasicProcessor): 8 | processor_type = ProcessorType.YOUTUBE_TRANSCRIPT_INPUT 9 | 10 | def __init__(self, config): 11 | super().__init__(config) 12 | self.url = config["url"] 13 | self.language = config["language"] 14 | 15 | def process(self): 16 | loader = YoutubeLoader.from_youtube_url( 17 | self.url, 18 | add_video_info=True, 19 | language=self.language, 20 | ) 21 | 22 | documents = loader.load() 23 | content = " ".join(doc.page_content for doc in documents) 24 | 25 | if content == "": 26 | raise Exception(f"No transcription found for {self.url}") 27 | 28 | return content 29 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/extension/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/backend/app/processors/components/extension/__init__.py -------------------------------------------------------------------------------- /packages/backend/app/processors/components/extension/extension_processor.py: -------------------------------------------------------------------------------- 1 | from ..model import NodeConfig 2 | from ...context.processor_context import ProcessorContext 3 | from ..processor import BasicProcessor, ContextAwareProcessor 4 | 5 | 6 | class ExtensionProcessor: 7 | """Base interface for extension processors""" 8 | 9 | def get_node_config(self) -> NodeConfig: 10 | pass 11 | 12 | 13 | class DynamicExtensionProcessor: 14 | """Base interface for dynamic extension processors - These nodes config are populated by an API call after a user choice""" 15 | 16 | def get_dynamic_node_config(self, data) -> NodeConfig: 17 | pass 18 | 19 | 20 | class BasicExtensionProcessor(ExtensionProcessor, BasicProcessor): 21 | """A basic extension processor that does not depend on user-specific parameters. 22 | 23 | Inherits basic processing capabilities from BasicProcessor and schema handling from ExtensionProcessor. 24 | 25 | Args: 26 | config (dict): Configuration dictionary for processor setup. 27 | """ 28 | 29 | def __init__(self, config): 30 | super().__init__(config) 31 | 32 | 33 | class ContextAwareExtensionProcessor(ExtensionProcessor, ContextAwareProcessor): 34 | """An extension processor that requires context about the user, such as user-specific settings or keys. 35 | 36 | This class supports context-aware processing by incorporating user context into the processing flow. 37 | 38 | Args: 39 | config (dict): Configuration dictionary for processor setup. 40 | context (ProcessorContext, optional): Context object containing user-specific parameters. Defaults to None. 41 | """ 42 | 43 | def __init__(self, config, context: ProcessorContext = None): 44 | super().__init__(config) 45 | self._processor_context = context 46 | -------------------------------------------------------------------------------- /packages/backend/app/processors/components/node_config_utils.py: -------------------------------------------------------------------------------- 1 | from .model import NodeConfigVariant 2 | 3 | 4 | def get_sub_configuration(discriminators_values, node_config: NodeConfigVariant): 5 | for subconfig in node_config.subConfigurations: 6 | subconfig_discriminator_values = [ 7 | subconfig.discriminators[discriminator] 8 | for discriminator in subconfig.discriminators 9 | ] 10 | if subconfig_discriminator_values == discriminators_values: 11 | return subconfig 12 | -------------------------------------------------------------------------------- /packages/backend/app/processors/context/processor_context.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | from typing import List 4 | 5 | 6 | class ProcessorContext(ABC): 7 | @abstractmethod 8 | def get_context(self) -> "ProcessorContext": 9 | pass 10 | 11 | @abstractmethod 12 | def get_current_user_id(self) -> Optional[str]: 13 | pass 14 | 15 | @abstractmethod 16 | def get_session_id(self) -> Optional[str]: 17 | pass 18 | 19 | @abstractmethod 20 | def get_parameter_names(self) -> List[str]: 21 | """ 22 | List all the parameter names currently stored in the context. 23 | Returns: 24 | A list of parameter names. 25 | """ 26 | pass 27 | 28 | @abstractmethod 29 | def get_value(self, name) -> Optional[str]: 30 | """ 31 | Retrieve the value associated with the specified parameter name. 32 | Returns: 33 | The value of the parameter if found, otherwise None. 34 | """ 35 | pass 36 | -------------------------------------------------------------------------------- /packages/backend/app/processors/context/processor_context_flask_request.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from ...flask.utils.constants import SESSION_USER_ID_KEY 3 | from .processor_context import ProcessorContext 4 | 5 | from copy import deepcopy 6 | 7 | 8 | class ProcessorContextFlaskRequest(ProcessorContext): 9 | parameter_prefix = "session_" 10 | 11 | def __init__(self, g_context=None, session_data=None, session_id=None): 12 | self.g_context = deepcopy(g_context) if g_context is not None else {} 13 | self.session_data = deepcopy(session_data) if session_data is not None else {} 14 | self.session_id = deepcopy(session_id) if session_id is not None else None 15 | 16 | def get_context(self) -> "ProcessorContext": 17 | """Retrieve the stored Flask global context.""" 18 | return self.g_context 19 | 20 | def get_current_user_id(self) -> str: 21 | """Retrieve the current user ID from the stored session data.""" 22 | return self.session_data.get(SESSION_USER_ID_KEY) 23 | 24 | def get_session_id(self) -> str: 25 | return self.session_id 26 | 27 | def get_parameter_names(self) -> List[str]: 28 | return [ 29 | key.replace(self.parameter_prefix, "") 30 | for key in dir(self.g_context) 31 | if not key.startswith("_") and key not in dir(type(self.g_context)) 32 | ] 33 | 34 | def get_value(self, name) -> Optional[str]: 35 | return self.g_context.get(self.parameter_prefix + name) 36 | -------------------------------------------------------------------------------- /packages/backend/app/processors/factory/processor_factory.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from ...storage.storage_strategy import StorageStrategy 4 | from ..context.processor_context import ProcessorContext 5 | 6 | 7 | class ProcessorFactory(ABC): 8 | @abstractmethod 9 | def create_processor( 10 | self, 11 | config, 12 | context: ProcessorContext = None, 13 | storage_strategy: StorageStrategy = None, 14 | ): 15 | pass 16 | 17 | @abstractmethod 18 | def load_processors(self): 19 | pass 20 | -------------------------------------------------------------------------------- /packages/backend/app/processors/launcher/basic_processor_launcher.py: -------------------------------------------------------------------------------- 1 | from .abstract_topological_processor_launcher import AbstractTopologicalProcessorLauncher 2 | 3 | 4 | class BasicProcessorLauncher(AbstractTopologicalProcessorLauncher): 5 | """ 6 | Basic Processor Launcher emiting event 7 | 8 | A class that launches processors based on configuration data. 9 | """ 10 | 11 | def launch_processors(self, processors): 12 | for processor in processors.values(): 13 | self.notify_current_node_running(processor) 14 | try : 15 | output = processor.process() 16 | self.notify_progress(processor, output) 17 | 18 | except Exception as e: 19 | self.notify_error(processor, e) 20 | raise e 21 | 22 | def launch_processors_for_node(self, processors, node_name=None): 23 | for processor in processors.values(): 24 | if processor.get_output() is None or processor.name == node_name: 25 | 26 | self.notify_current_node_running(processor) 27 | try : 28 | output = processor.process() 29 | self.notify_progress(processor, output) 30 | 31 | except Exception as e: 32 | self.notify_error(processor, e) 33 | raise e 34 | 35 | if processor.name == node_name: 36 | break 37 | -------------------------------------------------------------------------------- /packages/backend/app/processors/launcher/event_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EventType(Enum): 5 | PROGRESS = "progress" 6 | STREAMING = "streaming" 7 | CURRENT_NODE_RUNNING = "current_node_running" 8 | ERROR = "error" 9 | -------------------------------------------------------------------------------- /packages/backend/app/processors/launcher/processor_event.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Any 3 | 4 | 5 | @dataclass 6 | class ProcessorEvent: 7 | source: Any = field(default=None) 8 | output: Any = field(default=None) 9 | error: str = field(default=None) 10 | -------------------------------------------------------------------------------- /packages/backend/app/processors/launcher/processor_launcher.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from ..context.processor_context import ProcessorContext 4 | 5 | 6 | class ProcessorLauncher(ABC): 7 | @abstractmethod 8 | def load_processors(self, config_data): 9 | pass 10 | 11 | @abstractmethod 12 | def load_processors_for_node(self, config_data, node_name): 13 | pass 14 | 15 | @abstractmethod 16 | def launch_processors(self, processor): 17 | pass 18 | 19 | @abstractmethod 20 | def launch_processors_for_node(self, processors, node_name): 21 | pass 22 | 23 | @abstractmethod 24 | def set_context(self, context: ProcessorContext): 25 | pass 26 | -------------------------------------------------------------------------------- /packages/backend/app/processors/launcher/processor_launcher_event.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Any 3 | 4 | from ..components.processor import Processor 5 | 6 | 7 | @dataclass 8 | class ProcessorLauncherEvent: 9 | instance_name: str 10 | user_id: int = field(default=None) 11 | output: Any = field(default=None) 12 | processor_type: str = field(default=None) 13 | processor: Processor = field(default=None) 14 | isDone: bool = field(default=False) 15 | error: str = field(default=None) 16 | session_id: str = field(default=None) 17 | duration: float = field(default=0) 18 | -------------------------------------------------------------------------------- /packages/backend/app/processors/observer/observer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Observer(ABC): 5 | @abstractmethod 6 | def notify(self, event, data): 7 | pass 8 | -------------------------------------------------------------------------------- /packages/backend/app/processors/observer/socketio_event_emitter.py: -------------------------------------------------------------------------------- 1 | from ..launcher.event_type import EventType 2 | from ..launcher.processor_launcher_event import ProcessorLauncherEvent 3 | 4 | from .observer import Observer 5 | import logging 6 | from ...flask.socketio_init import socketio 7 | 8 | 9 | class SocketIOEventEmitter(Observer): 10 | """ 11 | A SocketIO event emitter that emits events to clients connected via WebSocket. 12 | 13 | This class implements the Observer pattern and is designed to emit events 14 | to specific client sessions in a Flask-SocketIO application. It can be safely 15 | executed within greenthreads, making it suitable for use in environments 16 | where asynchronous operations and real-time communication are required. 17 | 18 | Attributes: 19 | None 20 | 21 | Methods: 22 | notify(event, data): Emits the specified event to the client associated 23 | with the session ID in `data`. Handles exceptions 24 | gracefully and logs emission details. 25 | """ 26 | 27 | def notify(self, event: EventType, data: ProcessorLauncherEvent): 28 | if event == EventType.STREAMING.value: 29 | event = EventType.PROGRESS.value 30 | 31 | json_event = {} 32 | 33 | json_event["instanceName"] = data.instance_name 34 | 35 | if data.output is not None: 36 | json_event["output"] = data.output 37 | 38 | if data.isDone is not None: 39 | json_event["isDone"] = data.isDone 40 | 41 | if data.error is not None: 42 | json_event["error"] = str(data.error) 43 | 44 | try: 45 | socketio.emit(event, json_event, to=data.session_id) 46 | logging.debug( 47 | f"Successfully emitted event {event} with data {json_event} to {data.session_id}" 48 | ) 49 | except Exception as e: 50 | logging.error(f"Error emitting event {event}: {e}") 51 | -------------------------------------------------------------------------------- /packages/backend/app/providers/template/template_provider.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict 2 | 3 | 4 | class TemplateProvider: 5 | def get_templates(self, cursor=None): 6 | raise NotImplementedError("This method should be implemented by subclasses.") 7 | 8 | def get_template_by_id(self, template_id: int) -> Optional[Dict]: 9 | pass 10 | 11 | def save_template(self, template_data: Dict): 12 | pass 13 | -------------------------------------------------------------------------------- /packages/backend/app/storage/local_storage_strategy.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from ..storage.storage_strategy import StorageStrategy 3 | from werkzeug.utils import secure_filename 4 | import os 5 | from app.env_config import ( 6 | get_local_storage_folder_path, 7 | ) 8 | from injector import singleton 9 | 10 | 11 | @singleton 12 | class LocalStorageStrategy(StorageStrategy): 13 | """Local storage strategy. To be used only when you're running the app on your own machine. 14 | Every generated image is saved in a local directory.""" 15 | 16 | LOCAL_DIR = get_local_storage_folder_path() 17 | 18 | def save(self, filename: str, data: Any) -> str: 19 | if not os.path.exists(self.LOCAL_DIR): 20 | os.makedirs(self.LOCAL_DIR) 21 | 22 | secure_name = secure_filename(filename) 23 | filepath = os.path.join(self.LOCAL_DIR, secure_name) 24 | with open(filepath, "wb") as f: 25 | f.write(data) 26 | 27 | return self.get_url(secure_name) 28 | 29 | def get_url(self, filename: str) -> str: 30 | port = os.getenv("PORT") 31 | return f"http://localhost:{port}/image/{filename}" 32 | -------------------------------------------------------------------------------- /packages/backend/app/storage/storage_strategy.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Optional 3 | 4 | 5 | class StorageStrategy(ABC): 6 | """Storage strategy interface. We use this storage strategy to save and get the url of documents. 7 | This is especially useful for the image generated by the stable diffusion model.""" 8 | 9 | @abstractmethod 10 | def save(self, filename: str, data: Any) -> Optional[str]: 11 | pass 12 | 13 | @abstractmethod 14 | def get_url(self, filename: str) -> str: 15 | pass 16 | 17 | 18 | class CloudStorageStrategy(StorageStrategy): 19 | @abstractmethod 20 | def get_upload_link(self, filename: str) -> str: 21 | pass 22 | -------------------------------------------------------------------------------- /packages/backend/app/tasks/green_pool_task_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from queue import Queue 3 | import eventlet 4 | from eventlet.green import threading 5 | 6 | 7 | from .task_exception import TaskAlreadyRegisteredError 8 | 9 | from ..env_config import get_background_task_max_workers 10 | 11 | task_queues = {} 12 | task_processors = {} 13 | task_semaphores = {} 14 | 15 | pool = eventlet.GreenPool(size=get_background_task_max_workers()) 16 | 17 | 18 | def register_task_processor(task_name, processor_func, max_concurrent_tasks=2): 19 | if task_name in task_queues: 20 | raise TaskAlreadyRegisteredError(task_name=task_name) 21 | 22 | task_queue = Queue() 23 | task_queues[task_name] = task_queue 24 | task_processors[task_name] = processor_func 25 | task_semaphores[task_name] = threading.Semaphore(max_concurrent_tasks) 26 | 27 | logging.info( 28 | f"Registered green pool task processor '{task_name}' with max_concurrent_tasks={max_concurrent_tasks}" 29 | ) 30 | 31 | 32 | def process_task(task_name, task_data, task_result_queue): 33 | semaphore = task_semaphores.get(task_name) 34 | if semaphore is not None: 35 | with semaphore: 36 | if task_name in task_processors: 37 | processor_func = task_processors[task_name] 38 | result = processor_func(task_data) 39 | task_result_queue.put(result) 40 | else: 41 | raise ValueError(f"Nao task processor registered for {task_name}") 42 | else: 43 | raise ValueError(f"No semaphore registered for {task_name}") 44 | 45 | 46 | def add_task(task_name, task_data, result_queue): 47 | if task_name in task_queues: 48 | return pool.spawn(process_task, task_name, task_data, result_queue) 49 | else: 50 | raise ValueError(f"No task processor registered for {task_name}") 51 | -------------------------------------------------------------------------------- /packages/backend/app/tasks/task_exception.py: -------------------------------------------------------------------------------- 1 | class TaskAlreadyRegisteredError(Exception): 2 | """Exception raised when attempting to register a task that is already registered.""" 3 | 4 | def __init__(self, task_name): 5 | self.task_name = task_name 6 | super().__init__(f"Task '{task_name}' is already registered.") 7 | -------------------------------------------------------------------------------- /packages/backend/app/tasks/task_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from queue import Queue 3 | from concurrent.futures import ThreadPoolExecutor 4 | import threading 5 | 6 | from .task_exception import TaskAlreadyRegisteredError 7 | 8 | from ..env_config import get_background_task_max_workers 9 | 10 | task_queues = {} 11 | task_processors = {} 12 | task_semaphores = {} 13 | 14 | executor = ThreadPoolExecutor(max_workers=get_background_task_max_workers()) 15 | 16 | 17 | def register_task_processor(task_name, processor_func, max_concurrent_tasks=2): 18 | if task_name in task_queues: 19 | raise TaskAlreadyRegisteredError(task_name=task_name) 20 | 21 | task_queue = Queue() 22 | task_queues[task_name] = task_queue 23 | task_processors[task_name] = processor_func 24 | task_semaphores[task_name] = threading.Semaphore(max_concurrent_tasks) 25 | 26 | logging.info( 27 | f"Registered task processor '{task_name}' with max_concurrent_tasks={max_concurrent_tasks}" 28 | ) 29 | 30 | 31 | def process_task(task_name, task_data, task_result_queue): 32 | semaphore = task_semaphores.get(task_name) 33 | if semaphore is not None: 34 | with semaphore: 35 | if task_name in task_processors: 36 | processor_func = task_processors[task_name] 37 | result = processor_func(task_data) 38 | task_result_queue.put(result) 39 | else: 40 | raise ValueError(f"No task processor registered for {task_name}") 41 | else: 42 | raise ValueError(f"No semaphore registered for {task_name}") 43 | 44 | 45 | def add_task(task_name, task_data, result_queue): 46 | if task_name in task_queues: 47 | executor.submit(process_task, task_name, task_data, result_queue) 48 | else: 49 | raise ValueError(f"No task processor registered for {task_name}") 50 | -------------------------------------------------------------------------------- /packages/backend/app/tasks/task_utils.py: -------------------------------------------------------------------------------- 1 | from queue import Empty 2 | import time 3 | import eventlet 4 | 5 | 6 | def wait_for_result(queue, timeout=120, initial_sleep=0.1, max_sleep=5.0): 7 | start_time = time.time() 8 | sleep_duration = initial_sleep 9 | 10 | while True: 11 | try: 12 | result = queue.get_nowait() 13 | return result 14 | except Empty: 15 | if time.time() - start_time >= timeout: 16 | raise TimeoutError("Operation timed out after the specified timeout") 17 | 18 | eventlet.sleep(sleep_duration) 19 | sleep_duration = min(sleep_duration * 1.5, max_sleep) 20 | -------------------------------------------------------------------------------- /packages/backend/app/tasks/thread_pool_task_manager.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor 2 | import logging 3 | from queue import Queue 4 | import threading 5 | 6 | 7 | from .task_exception import TaskAlreadyRegisteredError 8 | 9 | from ..env_config import get_background_task_max_workers 10 | 11 | task_queues = {} 12 | task_processors = {} 13 | task_semaphores = {} 14 | 15 | executor = ThreadPoolExecutor(max_workers=get_background_task_max_workers()) 16 | 17 | 18 | def register_task_processor(task_name, processor_func, max_concurrent_tasks=2): 19 | if task_name in task_queues: 20 | raise TaskAlreadyRegisteredError(task_name=task_name) 21 | 22 | task_queue = Queue() 23 | task_queues[task_name] = task_queue 24 | task_processors[task_name] = processor_func 25 | task_semaphores[task_name] = threading.Semaphore(max_concurrent_tasks) 26 | 27 | logging.info( 28 | f"Registered thread pool task processor '{task_name}' with max_concurrent_tasks={max_concurrent_tasks}" 29 | ) 30 | 31 | 32 | def process_task(task_name, task_data, task_result_queue): 33 | semaphore = task_semaphores.get(task_name) 34 | if semaphore is not None: 35 | with semaphore: 36 | if task_name in task_processors: 37 | processor_func = task_processors[task_name] 38 | result = processor_func(task_data) 39 | task_result_queue.put(result) 40 | else: 41 | raise ValueError(f"Nao task processor registered for {task_name}") 42 | else: 43 | raise ValueError(f"No semaphore registered for {task_name}") 44 | 45 | 46 | def add_task(task_name, task_data, result_queue): 47 | if task_name in task_queues: 48 | return executor.submit(process_task, task_name, task_data, result_queue) 49 | else: 50 | raise ValueError(f"No task processor registered for {task_name}") 51 | -------------------------------------------------------------------------------- /packages/backend/config.yaml: -------------------------------------------------------------------------------- 1 | core: 2 | openai_api_key: 3 | tag: "core" 4 | description: "API key for accessing OpenAI services." 5 | 6 | stabilityai_api_key: 7 | tag: "core" 8 | description: "API key for accessing Stability AI services." 9 | 10 | replicate_api_key: 11 | tag: "core" 12 | description: "API key for accessing Replicate services." 13 | 14 | extension: 15 | anthropic_api_key: 16 | tag: "core" 17 | description: "API key for accessing Anthropic services." 18 | 19 | deepseek_api_key: 20 | tag: "core" 21 | description: "API key for accessing DeepSeek services." 22 | 23 | openrouter_api_key: 24 | tag: "core" 25 | description: "API key for accessing OpenRouter services." -------------------------------------------------------------------------------- /packages/backend/hooks/hook-app.processors.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | 3 | hiddenimports = collect_submodules('app.processors') -------------------------------------------------------------------------------- /packages/backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ai-flow-back" 3 | version = "0.10.0" 4 | description = "" 5 | authors = ["DahnM20 "] 6 | readme = "README.md" 7 | packages = [{ include = "app" }] 8 | 9 | 10 | [tool.poetry.dependencies] 11 | python = ">=3.9,<3.12" 12 | python-dotenv = "^1.0.0" 13 | openai = "1.76.0" 14 | flask = "^2.2.3" 15 | flask-socketio = "^5.3.3" 16 | flask-cors = "^3.0.10" 17 | unstructured = "^0.6.3" 18 | langchain = ">=0.0.303" 19 | python-magic = "^0.4.14" 20 | pytesseract = "^0.3.10" 21 | requests = "^2.31.0" 22 | tabulate = "^0.9.0" 23 | pdf2image = "^1.16.3" 24 | colorlog = "^6.7.0" 25 | eventlet = "^0.33.3" 26 | playwright = "1.39" 27 | youtube-transcript-api = "^0.6.1" 28 | pytube = "^15.0.0" 29 | boto3 = "^1.28.52" 30 | pyjwt = "^2.8.0" 31 | jwcrypto = "^1.5.0" 32 | python-jose = "^3.3.0" 33 | cachetools = "^5.3.1" 34 | flask-injector = "^0.15.0" 35 | setuptools = "^68.2.2" 36 | pypdf = "4.2.0" 37 | replicate = "0.22.0" 38 | tiktoken = "0.7.0" 39 | llama-index-llms-openai = "v0.1.26" 40 | llama-index-multi-modal-llms-openai = "^0.1.6" 41 | llama-index = "v0.10.56" 42 | openapi-spec-validator = "^0.7.1" 43 | anthropic = "0.49.0" 44 | pymupdf = "^1.24.7" 45 | pydub = "^0.25.1" 46 | markdownify = "^1.1.0" 47 | beautifulsoup4 = "^4.13.3" 48 | 49 | 50 | [tool.poetry.group.dev.dependencies] 51 | datamodel-code-generator = "^0.25.5" 52 | 53 | [build-system] 54 | requires = ["poetry-core"] 55 | build-backend = "poetry.core.masonry.api" 56 | -------------------------------------------------------------------------------- /packages/backend/requirements_windows.txt: -------------------------------------------------------------------------------- 1 | python-magic-bin -------------------------------------------------------------------------------- /packages/backend/server.py: -------------------------------------------------------------------------------- 1 | from app.log_config import root_logger 2 | import sys 3 | import os 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | 9 | from app.flask.socketio_init import flask_app, socketio 10 | import app.flask.sockets 11 | import app.flask.routes 12 | import app.tasks.single_thread_tasks.browser.async_browser_task 13 | 14 | 15 | if __name__ == "__main__": 16 | host = os.getenv("HOST", "127.0.0.1") 17 | port = int(os.getenv("PORT", 8000)) 18 | 19 | root_logger.info(f"Starting application on {host}:{port}...") 20 | root_logger.info("You can stop the application by pressing Ctrl+C at any time.") 21 | 22 | # If we're running in a PyInstaller bundle 23 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 24 | os.environ["PLAYWRIGHT_BROWSERS_PATH"] = os.path.join( 25 | sys._MEIPASS, "ms-playwright" 26 | ) 27 | 28 | if os.getenv("USE_HTTPS", "false").lower() == "true": 29 | root_logger.info("Protocol set to HTTPS") 30 | keyfile_path = os.getenv("KEYFILE_PATH", "default/key/path") 31 | certfile_path = os.getenv("CERTFILE_PATH", "default/cert/path") 32 | socketio.run( 33 | flask_app, 34 | host=host, 35 | port=port, 36 | keyfile=keyfile_path, 37 | certfile=certfile_path, 38 | ) 39 | else: 40 | root_logger.warning("Protocol set to HTTP") 41 | socketio.run(flask_app, port=port, host=host) 42 | -------------------------------------------------------------------------------- /packages/backend/tests/unit/test_ai_data_splitter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import ANY, patch 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | from app.flask.socketio_init import flask_app, socketio 9 | 10 | from app.processors.components.core.ai_data_splitter_processor import ( 11 | AIDataSplitterProcessor, 12 | ) 13 | from app.processors.components.core.input_processor import InputProcessor 14 | from tests.utils.llm_factory_mock import LLMMockFactory 15 | from tests.utils.processor_context_mock import ProcessorContextMock 16 | 17 | 18 | class TestAIDataSplitter(unittest.TestCase): 19 | @staticmethod 20 | def get_default_valid_config(): 21 | return { 22 | "inputs": [{"inputNode": "4f2d3sh03#input-processor"}], 23 | "name": "s69w5eiha#ai-data-splitter", 24 | "processorType": "ai-data-splitter", 25 | "nbOutput": 0, 26 | } 27 | 28 | def test_process_with_missing_input_return_none_and_does_not_call_openai_api(self): 29 | # Arrange 30 | MODEL = "gpt-4" 31 | API_KEY = "000000000" 32 | MOCKED_RESPONSE = "A dog;A dolphin;An elephant" 33 | 34 | config = self.get_default_valid_config() 35 | 36 | context = ProcessorContextMock(API_KEY) 37 | 38 | mock_factory = LLMMockFactory(expected_response=MOCKED_RESPONSE) 39 | processor = AIDataSplitterProcessor(config, context, mock_factory) 40 | 41 | # Act 42 | result = processor.process() 43 | 44 | # Assert 45 | self.assertEqual(result, "") 46 | -------------------------------------------------------------------------------- /packages/backend/tests/unit/test_llm_prompt_processor.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import ANY, patch 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | from app.flask.socketio_init import socketio 9 | 10 | from app.processors.components.core.llm_prompt_processor import ( 11 | LLMPromptProcessor, 12 | ) 13 | from tests.utils.llm_factory_mock import LLMMockFactory 14 | from tests.utils.processor_context_mock import ProcessorContextMock 15 | 16 | 17 | class TestLLMPromptProcessor(unittest.TestCase): 18 | @staticmethod 19 | def get_default_valid_config(model): 20 | return { 21 | "input": [], 22 | "name": "g1ei670qc#llm-prompt", 23 | "processorType": "llm-prompt", 24 | "model": model, 25 | "prompt": "Generate a short poem about a bird and a butterfly", 26 | "x": "-911.2707176494437", 27 | "y": "-251.00226805963663", 28 | } 29 | -------------------------------------------------------------------------------- /packages/backend/tests/unit/test_processor_launcher.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import MagicMock, Mock, patch, mock_open 3 | 4 | from app.processors.launcher.basic_processor_launcher import BasicProcessorLauncher 5 | from app.processors.factory.processor_factory_iter_modules import ( 6 | ProcessorFactoryIterModules, 7 | ) 8 | 9 | from dotenv import load_dotenv 10 | 11 | from app.tasks.single_thread_tasks.browser.browser_task import ( 12 | stop_browser_thread, 13 | ) 14 | 15 | load_dotenv() 16 | 17 | from app.flask.socketio_init import socketio 18 | 19 | 20 | class NoInputMock(MagicMock): 21 | def __getattr__(self, name): 22 | if name == "inputs": 23 | raise AttributeError( 24 | f"'{type(self).__name__}' object has no attribute 'inputs'" 25 | ) 26 | return super().__getattr__(name) 27 | 28 | 29 | class TestProcessorLauncher(unittest.TestCase): 30 | def test_load_config_data_valid_file(self): 31 | factory = ProcessorFactoryIterModules() 32 | launcher = BasicProcessorLauncher(factory, None) 33 | 34 | m = mock_open(read_data='{"key": "value"}') 35 | with patch("builtins.open", m): 36 | data = launcher._load_config_data("fake_file_path") 37 | self.assertEqual(data, {"key": "value"}) 38 | 39 | def test_link_processors_valid(self): 40 | factory = ProcessorFactoryIterModules() 41 | launcher = BasicProcessorLauncher(factory, None) 42 | 43 | processor1 = Mock() 44 | processor1.name = "processor1" 45 | processor1.inputs = [{"inputNode": "processor2"}] 46 | 47 | processor2 = NoInputMock() 48 | processor2.name = "processor2" 49 | 50 | processors = { 51 | "processor1": processor1, 52 | "processor2": processor2, 53 | } 54 | 55 | launcher._link_processors(processors) 56 | processor1.add_input_processor.assert_called_once_with(processor2) 57 | 58 | stop_browser_thread() 59 | -------------------------------------------------------------------------------- /packages/backend/tests/utils/llm_factory_mock.py: -------------------------------------------------------------------------------- 1 | from app.llms.factory.llm_factory import LLMFactory 2 | from tests.utils.llm_mock import LLMMock 3 | from llama_index.core.base.llms.base import BaseLLM 4 | 5 | 6 | class LLMMockFactory(LLMFactory): 7 | def __init__(self, **kwargs): 8 | super().__init__() 9 | self.expected_response = kwargs.get("expected_response", None) 10 | 11 | def create_llm(self, model: str, **kwargs) -> BaseLLM: 12 | return LLMMock(expected_response=self.expected_response) 13 | -------------------------------------------------------------------------------- /packages/backend/tests/utils/openai_mock_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | 4 | def create_mocked_openai_response( 5 | model="gpt-4", api_key="000000000", response_content="Mocked Response" 6 | ): 7 | """ 8 | Create a mocked response for OpenAI. 9 | 10 | :param model: The model to be used. 11 | :param api_key: The API key to be used. 12 | :param response_content: The content for the mocked response. 13 | :return: A mocked response for OpenAI. 14 | """ 15 | mock_message = Mock() 16 | mock_message.content = response_content 17 | 18 | mock_choice = Mock() 19 | mock_choice.message = mock_message 20 | 21 | mock_response = Mock() 22 | mock_response.choices = [mock_choice] 23 | 24 | return mock_response 25 | -------------------------------------------------------------------------------- /packages/backend/tests/utils/processor_context_mock.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from app.processors.context.processor_context import ProcessorContext 3 | from typing import List 4 | 5 | 6 | class ProcessorContextMock(ProcessorContext): 7 | def __init__(self, api_key, user_id=0, session_id=0) -> None: 8 | super().__init__() 9 | self.api_key = api_key 10 | self.user_id = user_id 11 | self.session_id = session_id 12 | 13 | def get_context(self): 14 | return self.api_key 15 | 16 | def get_current_user_id(self): 17 | return self.user_id 18 | 19 | def get_session_id(self): 20 | return self.user_id 21 | 22 | def get_parameter_names(self) -> List[str]: 23 | return super().get_parameter_names() 24 | 25 | def get_value(self, name): 26 | if "api_key" in name: 27 | return self.api_key 28 | return super().get_value(name) 29 | 30 | def is_using_personal_keys(self, source_name): 31 | return False 32 | -------------------------------------------------------------------------------- /packages/ui/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_WS_HOST=localhost 2 | VITE_APP_WS_PORT=5000 3 | VITE_APP_API_REST_PORT=5000 4 | VITE_APP_USE_HTTPS=false 5 | VITE_APP_VERSION=0.7.0 6 | VITE_APP_USE_CACHE=true 7 | VITE_APP_DEFAULT_NODES_HIDDEN_LIST=stable-diffusion-stabilityai-prompt,ai-action -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #amplify-do-not-edit-begin 26 | amplify/\#current-cloud-backend 27 | amplify/.config/local-* 28 | amplify/logs 29 | amplify/mock-data 30 | amplify/mock-api-resources 31 | amplify/backend/amplify-meta.json 32 | amplify/backend/.temp 33 | build/ 34 | dist/ 35 | node_modules/ 36 | aws-exports.js 37 | awsconfiguration.json 38 | amplifyconfiguration.json 39 | amplifyconfiguration.dart 40 | amplify-build-config.json 41 | amplify-gradle-config.json 42 | amplifytools.xcconfig 43 | .secret-* 44 | **.sample 45 | #amplify-do-not-edit-end 46 | -------------------------------------------------------------------------------- /packages/ui/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Ignore artifacts: 4 | build 5 | coverage -------------------------------------------------------------------------------- /packages/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21 as build 2 | 3 | WORKDIR /app 4 | 5 | ARG VITE_APP_WS_HOST 6 | ARG VITE_APP_WS_PORT 7 | ARG VITE_APP_API_REST_PORT 8 | ARG VITE_APP_USE_HTTPS 9 | ARG VITE_APP_VERSION 10 | 11 | ENV VITE_APP_WS_HOST=$VITE_APP_WS_HOST 12 | ENV VITE_APP_WS_PORT=$VITE_APP_WS_PORT 13 | ENV VITE_APP_API_REST_PORT=$VITE_APP_API_REST_PORT 14 | ENV VITE_APP_USE_HTTPS=$VITE_APP_USE_HTTPS 15 | ENV VITE_APP_VERSION=$VITE_APP_VERSION 16 | 17 | COPY package.json package-lock.json /app/ 18 | 19 | RUN npm ci 20 | 21 | COPY . /app/ 22 | 23 | RUN npm run build 24 | RUN ls -al /app 25 | 26 | FROM nginx:1.21 27 | 28 | COPY --from=build ./app/build /usr/share/nginx/html 29 | 30 | COPY nginx.conf /etc/nginx/conf.d/default.conf 31 | 32 | EXPOSE 80 33 | 34 | CMD ["nginx", "-g", "daemon off;"] 35 | -------------------------------------------------------------------------------- /packages/ui/amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "aiflow", 3 | "version": "3.1", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /packages/ui/amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /packages/ui/amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "aiflowfa7513ae": { 4 | "customAuth": false, 5 | "dependsOn": [], 6 | "frontendAuthConfig": { 7 | "mfaConfiguration": "OFF", 8 | "mfaTypes": [ 9 | "SMS" 10 | ], 11 | "passwordProtectionSettings": { 12 | "passwordPolicyCharacters": [], 13 | "passwordPolicyMinLength": 8 14 | }, 15 | "signupAttributes": [ 16 | "EMAIL" 17 | ], 18 | "socialProviders": [], 19 | "usernameAttributes": [ 20 | "EMAIL" 21 | ], 22 | "verificationMechanisms": [ 23 | "EMAIL" 24 | ] 25 | }, 26 | "providerPlugin": "awscloudformation", 27 | "service": "Cognito" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /packages/ui/amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /packages/ui/amplify/backend/types/amplify-dependent-resources-ref.d.ts: -------------------------------------------------------------------------------- 1 | export type AmplifyDependentResourcesAttributes = { 2 | "auth": { 3 | "aiflowfa7513ae": { 4 | "AppClientID": "string", 5 | "AppClientIDWeb": "string", 6 | "HostedUIDomain": "string", 7 | "IdentityPoolId": "string", 8 | "IdentityPoolName": "string", 9 | "OAuthMetadata": "string", 10 | "UserPoolArn": "string", 11 | "UserPoolId": "string", 12 | "UserPoolName": "string" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/ui/amplify/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Command Hooks 2 | 3 | Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. 4 | 5 | To get started, add your script files based on the expected naming convention in this directory. 6 | 7 | Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks 8 | -------------------------------------------------------------------------------- /packages/ui/amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-aiflow-dev-213601-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::073771317316:role/amplify-aiflow-dev-213601-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::073771317316:role/amplify-aiflow-dev-213601-authRole", 7 | "Region": "eu-west-3", 8 | "DeploymentBucketName": "amplify-aiflow-dev-213601-deployment", 9 | "UnauthRoleName": "amplify-aiflow-dev-213601-unauthRole", 10 | "StackName": "amplify-aiflow-dev-213601", 11 | "StackId": "arn:aws:cloudformation:eu-west-3:073771317316:stack/amplify-aiflow-dev-213601/ecf69d00-615a-11ee-9354-0a882ac490e6", 12 | "AmplifyAppId": "d1flwgvyncbanw" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "aiflowfa7513ae": {} 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /packages/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | AI Flow 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/ui/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | verbose: true, 5 | preset: "ts-jest", 6 | testEnvironment: "node", 7 | testMatch: ["**/test/**/*.ts", "**/?(*.)+(spec|test).ts"], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /packages/ui/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | 7 | location / { 8 | try_files $uri $uri/ /index.html; 9 | add_header Content-Security-Policy "script-src 'self' https://d24qo3tz1oj1bi.cloudfront.net https://ai-flow.auth.eu-west-3.amazoncognito.com 'unsafe-inline';"; 10 | } 11 | } -------------------------------------------------------------------------------- /packages/ui/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-preset-mantine": {}, 4 | "postcss-simple-vars": { 5 | variables: { 6 | "mantine-breakpoint-xs": "36em", 7 | "mantine-breakpoint-sm": "48em", 8 | "mantine-breakpoint-md": "62em", 9 | "mantine-breakpoint-lg": "75em", 10 | "mantine-breakpoint-xl": "88em", 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier-plugin-tailwindcss'], 3 | } -------------------------------------------------------------------------------- /packages/ui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/ui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/ui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/ui/public/backgrounds/g-particles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/backgrounds/g-particles.png -------------------------------------------------------------------------------- /packages/ui/public/backgrounds/g-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/backgrounds/g-simple.png -------------------------------------------------------------------------------- /packages/ui/public/curve-edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/ui/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/ui/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/favicon.ico -------------------------------------------------------------------------------- /packages/ui/public/gif-new-version.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/gif-new-version.gif -------------------------------------------------------------------------------- /packages/ui/public/handle-bottom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-left-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-right-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-top-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/handle-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/ui/public/health: -------------------------------------------------------------------------------- 1 | OK -------------------------------------------------------------------------------- /packages/ui/public/img/deepseek-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/img/deepseek-logo.png -------------------------------------------------------------------------------- /packages/ui/public/img/openrouter-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/img/openrouter-logo.jpg -------------------------------------------------------------------------------- /packages/ui/public/img/replicate-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/img/replicate-logo.png -------------------------------------------------------------------------------- /packages/ui/public/img/stabilityai-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/img/stabilityai-logo.jpg -------------------------------------------------------------------------------- /packages/ui/public/img/youtube-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/ui/public/locales/en/aiActions.json: -------------------------------------------------------------------------------- 1 | { 2 | "Summary": "Summary", 3 | "SpellCheck": "Spell Check", 4 | "VisualPrompt": "Visual Prompt", 5 | "ConstructiveCritique": "Constructive Critique", 6 | "SimpleExplanation": "Simple Explanation", 7 | "Paraphrase": "Paraphrase", 8 | "SentimentAnalysis": "Sentiment Analysis", 9 | "TextExtension": "Text Extension", 10 | "ClickToShowOutput": "Click to show output" 11 | } -------------------------------------------------------------------------------- /packages/ui/public/locales/en/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurationTitle": "Configuration", 3 | "apiKeyDisclaimer": "We do not use or store your API keys.", 4 | "openSourceDisclaimer": "The application code is open source.", 5 | "apiKeyRevokeReminder": "Remember, you can revoke your keys at any time and generate new ones.", 6 | "closeButtonLabel": "Close", 7 | "validateButtonLabel": "Validate", 8 | "likeProjectPrompt": "If you like this project, you can add a star on:", 9 | "supportProjectPrompt": "You can support the future of the project and contact us through", 10 | "Logout": "Logout", 11 | "sections.core": "Base parameters", 12 | "parameters.core.openai_api_key": "OpenAI API Key", 13 | "parameters.core.stabilityai_api_key": "StabilityAI API Key", 14 | "parameters.core.replicate_api_key": "Replicate API Key", 15 | "parameters.extension.anthropic_api_key": "Anthropic API Key", 16 | "sections.extension": "Extensions", 17 | "userTabLabel": "User parameters", 18 | "appParametersLabel": "App parameters", 19 | "displayTabLabel": "Display parameters", 20 | "nodesDisplayed": "Nodes enabled", 21 | "configUpdated": "Configuration updated successfully", 22 | "ShowMinimap": "Show minimap", 23 | "UI": "User Interface", 24 | "input": "Inputs", 25 | "models": "Models", 26 | "tools": "Tools", 27 | "parameters.extension.deepseek_api_key": "DeepSeek API Key", 28 | "parameters.extension.openrouter_api_key": "OpenRouter API Key" 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/public/locales/en/dialogs.json: -------------------------------------------------------------------------------- 1 | { 2 | "attachNodeTitle": "Attach Node", 3 | "attachNodeAction": "Attach" 4 | } -------------------------------------------------------------------------------- /packages/ui/public/locales/en/tour.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstTimeHere": "First time here?", 3 | "discoverApp": "Unlock tips to make the most of our app in just 15 seconds!", 4 | "iKnowTheApp": "I know the app", 5 | "letsStart": "Let's start!", 6 | "welcomeToAIFLOW": "Welcome to AI-FLOW", 7 | "addNodesWithDragAndDrop": "Easily add nodes to your canvas with a simple drag & drop.", 8 | "dragAndDrop": "Drag and Drop", 9 | "addingNodes": "Adding Nodes", 10 | "runningANode": "Running a Node", 11 | "connectingNodes": "Connecting Nodes", 12 | "runEverything": "Run Everything", 13 | "exploringMoreModels": "Exploring More Models", 14 | "youveGotTheBasics": "You've got the basics!", 15 | "executeSingleNode": "You can execute a single node by clicking the run button.", 16 | "runNode": "Run Node", 17 | "handlesExplanation": "Blue handles are for inputs, and orange handles are for outputs. For GPT Nodes, inputs add context to your prompts.", 18 | "connectNodes": "Connect Nodes", 19 | "executeAllNodesDescription": "This button executes all nodes in your flow, overwriting previous outputs.", 20 | "replicateNodeDescription": "Expand your capabilities with the Replicate Node, providing access to a wide range of models for advanced use-cases.", 21 | "replicateNode": "Replicate Node", 22 | "checkHelpForAdvanced": "For advanced use-cases, check the Help section at the bottom left.", 23 | "configDescription": "Here you can add your API Keys to be able to use the app.", 24 | "config": "Config" 25 | } -------------------------------------------------------------------------------- /packages/ui/public/locales/en/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "versionInfo": { 3 | "versionNumber": "v0.7.3", 4 | "description": "Discover the latest features added in version 0.7.3" 5 | }, 6 | "features": [ 7 | { 8 | "title": "Improved Web Extractor", 9 | "description": "You can now customized how data is extracted." 10 | }, 11 | { 12 | "title": "New Action: Help", 13 | "description": "Each node now includes a 'Help' action that enables you to learn how to use it." 14 | } 15 | ], 16 | "articles": [ 17 | { 18 | "title": "Generate Consistent Characters Using AI - Part 1", 19 | "url": "https://docs.ai-flow.net/blog/generate-consistent-characters-ai/" 20 | }, 21 | { 22 | "title": "How to automate story and image creation using AI - Part 2", 23 | "url": "https://docs.ai-flow.net/blog/automate-story-creation-2/" 24 | }, 25 | { 26 | "title": "How to use Documents in AI-FLOW", 27 | "url": "https://docs.ai-flow.net/blog/summarize-doc-post/" 28 | } 29 | ], 30 | "imageUrl": "https://ai-flow-public-assets.s3.eu-west-3.amazonaws.com/gif-v0.7.3.gif", 31 | "newVersionAvailable": "A new version is now available !", 32 | "newVersionDefaultMessage": "New features and bug fixes are available. To access them, please refresh your page.", 33 | "refresh": "Refresh" 34 | } 35 | -------------------------------------------------------------------------------- /packages/ui/public/locales/fr/aiActions.json: -------------------------------------------------------------------------------- 1 | { 2 | "Summary": "Résumé", 3 | "SpellCheck": "Vérification Orthographique", 4 | "VisualPrompt": "Prompt Visuelle", 5 | "ConstructiveCritique": "Critique Constructive", 6 | "SimpleExplanation": "Explication Simple", 7 | "Paraphrase": "Paraphrase", 8 | "SentimentAnalysis": "Analyse de Sentiment", 9 | "TextExtension": "Extension du Texte", 10 | "ClickToShowOutput": "Cliquez pour afficher le résultat" 11 | } -------------------------------------------------------------------------------- /packages/ui/public/locales/fr/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurationTitle": "Configuration", 3 | "apiKeyDisclaimer": "Nous n'utilisons ni ne stockons vos clés API.", 4 | "openSourceDisclaimer": "Ce projet est open source.", 5 | "apiKeyRevokeReminder": "N'oubliez pas, vous pouvez révoquer vos clés à tout moment et en générer de nouvelles.", 6 | "closeButtonLabel": "Fermer", 7 | "validateButtonLabel": "Valider", 8 | "likeProjectPrompt": "Si vous aimez ce projet, vous pouvez ajouter une étoile sur:", 9 | "supportProjectPrompt": "Vous pouvez soutenir l'avenir du projet et nous contacter via", 10 | "Logout": "Se déconnecter", 11 | "sections.core": "Paramètres de base", 12 | "parameters.core.openai_api_key": "Clé API OpenAI", 13 | "parameters.core.stabilityai_api_key": "Clé API StabilityAI", 14 | "parameters.core.replicate_api_key": "Clé API Replicate", 15 | "parameters.extension.anthropic_api_key": "Clé API Anthropic", 16 | "sections.extension": "Extensions", 17 | "userTabLabel": "Paramètres utilisateur", 18 | "appParametersLabel": "Paramètres de l'application", 19 | "displayTabLabel": "Paramètres d'affichage", 20 | "nodesDisplayed": "Nœuds activés", 21 | "configUpdated": "Configuration mise à jour avec succès", 22 | "ShowMinimap": "Afficher la minimap", 23 | "UI": "Interface Utilisateur", 24 | "input": "Entrées", 25 | "models": "Modèles", 26 | "tools": "Outils", 27 | "parameters.extension.deepseek_api_key": "Clé API DeepSeek", 28 | "parameters.extension.openrouter_api_key": "Clé API OpenRouter" 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/public/locales/fr/dialogs.json: -------------------------------------------------------------------------------- 1 | { 2 | "attachNodeTitle": "Attacher un noeud", 3 | "attachNodeAction": "Attacher" 4 | } -------------------------------------------------------------------------------- /packages/ui/public/locales/fr/tour.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstTimeHere": "Première visite ?", 3 | "discoverApp": "Découvrez des astuces pour profiter pleinement de notre application en moins de 15 secondes !", 4 | "iKnowTheApp": "Je connais l'application", 5 | "letsStart": "Commençons !", 6 | "welcomeToAIFLOW": "Bienvenue sur AI-FLOW", 7 | "addNodesWithDragAndDrop": "Ajoutez facilement des nœuds à votre canevas avec un simple glisser-déposer.", 8 | "dragAndDrop": "Glisser-Déposer", 9 | "addingNodes": "Ajouter un Nœud", 10 | "runningANode": "Exécuter un Nœud", 11 | "connectingNodes": "Connecter des Nœuds", 12 | "runEverything": "Tout Exécuter", 13 | "exploringMoreModels": "Explorer Plus de Modèles", 14 | "youveGotTheBasics": "Vous avez les bases !", 15 | "executeSingleNode": "Vous pouvez exécuter un seul nœud en cliquant sur le bouton d'exécution.", 16 | "runNode": "Exécuter un Nœud", 17 | "handlesExplanation": "Les poignées bleues sont pour les entrées, et les poignées oranges pour les sorties. Pour les Nœuds GPT, les entrées ajoutent du contexte à vos invites.", 18 | "connectNodes": "Connecter les Nœuds", 19 | "executeAllNodesDescription": "Ce bouton exécute tous les nœuds dans votre flux, en écrasant les sorties précédentes.", 20 | "replicateNodeDescription": "Étendez vos capacités avec le Nœud Replicate, offrant un accès à une large gamme de modèles pour des cas d'usage avancés.", 21 | "replicateNode": "Replicate", 22 | "checkHelpForAdvanced": "Pour des cas d'usage avancés, consultez la section Aide en bas à gauche.", 23 | "configDescription": "Vous pouvez ajouter vos clés APIs via ce menu pour utiliser l'application.", 24 | "config": "Configuration" 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/public/locales/fr/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "versionInfo": { 3 | "versionNumber": "v0.7.3", 4 | "description": "Voici les nouveautés de la v0.7.3" 5 | }, 6 | "features": [ 7 | { 8 | "title": "Amélioration de l'extracteur Web", 9 | "description": "Vous pouvez désormais mieux cibler l'extraction de données." 10 | }, 11 | { 12 | "title": "Nouvelle action : Aide", 13 | "description": "Chaque noeud possède désormais une action 'Aide' qui vous permet de découvrir comment l'utiliser." 14 | } 15 | ], 16 | "articles": [ 17 | { 18 | "title": "Générer des Personnages Cohérents avec l'IA - Partie 1", 19 | "url": "https://docs.ai-flow.net/fr/blog/generate-consistent-characters-ai/" 20 | }, 21 | { 22 | "title": "Comment automatiser la création d'histoires et d'images à l'aide de l'IA - Partie 2", 23 | "url": "https://docs.ai-flow.net/fr/blog/automate-story-creation-2/" 24 | }, 25 | { 26 | "title": "Comment Utiliser des Documents dans AI-FLOW", 27 | "url": "https://docs.ai-flow.net/fr/blog/summarize-doc-post/" 28 | } 29 | ], 30 | "imageUrl": "https://ai-flow-public-assets.s3.eu-west-3.amazonaws.com/gif-v0.7.3.gif", 31 | "newVersionAvailable": "Une nouvelle version est maintenant disponible !", 32 | "newVersionDefaultMessage": "De nouvelles fonctionnalités et des corrections de bugs sont disponibles, pour y accéder, veuillez rafraîchir votre page.", 33 | "refresh": "Rafraîchir" 34 | } 35 | -------------------------------------------------------------------------------- /packages/ui/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/logo.png -------------------------------------------------------------------------------- /packages/ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/ui/public/samples/intro.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "3jexlwros#llm-prompt", 5 | "processorType": "llm-prompt", 6 | "model": "gpt-4o", 7 | "x": -1130.048690482733, 8 | "y": -885.266525660136 9 | } 10 | ] -------------------------------------------------------------------------------- /packages/ui/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /packages/ui/public/smooth-step-edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/ui/public/step-edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/ui/public/straight-edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ui/public/tour-assets/tour-step-connect-nodes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/tour-assets/tour-step-connect-nodes.gif -------------------------------------------------------------------------------- /packages/ui/public/tour-assets/tour-step-drag-and-drop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/tour-assets/tour-step-drag-and-drop.gif -------------------------------------------------------------------------------- /packages/ui/public/tour-assets/tour-step-replicate-node.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/tour-assets/tour-step-replicate-node.gif -------------------------------------------------------------------------------- /packages/ui/public/tour-assets/tour-step-run-node.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DahnM20/ai-flow/3c15558fa146a5982b7ad3ca300d15c4f41142fe/packages/ui/public/tour-assets/tour-step-run-node.gif -------------------------------------------------------------------------------- /packages/ui/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import App from "./App"; 3 | import LoadingScreen from "./components/LoadingScreen"; 4 | 5 | const Main = () => { 6 | const [initialLoading, setInitialLoading] = useState(true); 7 | 8 | const handleLoadingComplete = () => { 9 | setInitialLoading(false); 10 | }; 11 | 12 | return ( 13 | <> 14 | {initialLoading && } 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Main; 21 | -------------------------------------------------------------------------------- /packages/ui/src/api/cache/cacheManager.ts: -------------------------------------------------------------------------------- 1 | import { isCacheEnabled } from "../../config/config"; 2 | 3 | interface CacheItem { 4 | data: T; 5 | ttl?: number; 6 | timestamp: number; 7 | } 8 | 9 | const DEFAULT_TTL = 3600 * 1000; // 1 hour 10 | const DEFAULT_NB_ELEMENTS_TO_REMOVE = 5; 11 | const DISPENSABLE_CACHE_PREFIX = "dispensable_cache"; 12 | 13 | export function generateCacheKey(functionName: string, ...args: any[]): string { 14 | const argsKey = JSON.stringify(args); 15 | return `${functionName}:${argsKey}`; 16 | } 17 | 18 | export function setCache(key: string, data: any, ttl?: number) { 19 | if (!isCacheEnabled()) return; 20 | const item = { 21 | data, 22 | ttl, 23 | timestamp: Date.now(), 24 | }; 25 | try { 26 | localStorage.setItem(key, JSON.stringify(item)); 27 | } catch (err: any) { 28 | if (err.code == 22 || err.code == 1014) { 29 | clearOldCacheItems(); 30 | localStorage.setItem(key, JSON.stringify(item)); 31 | } else { 32 | throw new Error(err.message); 33 | } 34 | } 35 | } 36 | 37 | export function getCache(key: string): T | undefined { 38 | if (!isCacheEnabled()) return; 39 | 40 | const itemStr = localStorage.getItem(key); 41 | if (!itemStr) return; 42 | 43 | const item = JSON.parse(itemStr) as CacheItem; 44 | const now = Date.now(); 45 | 46 | const ttl = item.ttl ?? DEFAULT_TTL; 47 | 48 | if (now - item.timestamp > ttl) { 49 | localStorage.removeItem(key); 50 | return; 51 | } 52 | 53 | return item.data; 54 | } 55 | 56 | function clearOldCacheItems() { 57 | const keys = Object.keys(localStorage); 58 | const items = keys 59 | .filter((key) => key.includes(DISPENSABLE_CACHE_PREFIX)) 60 | .map((key) => ({ 61 | key, 62 | data: JSON.parse(localStorage.getItem(key) ?? ""), 63 | })) 64 | .sort((a, b) => a.data.timestamp - b.data.timestamp); 65 | 66 | items.forEach((item, index) => { 67 | if (index <= DEFAULT_NB_ELEMENTS_TO_REMOVE) { 68 | localStorage.removeItem(item.key); 69 | } 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /packages/ui/src/api/cache/withCache.ts: -------------------------------------------------------------------------------- 1 | import { generateCacheKey, getCache, setCache } from "./cacheManager"; 2 | 3 | type AsyncFunction = (...args: T) => Promise; 4 | 5 | type Params = T extends (...args: infer U) => any ? U : never; 6 | 7 | interface CacheOptions { 8 | ttl: number; 9 | key?: string; 10 | } 11 | 12 | async function withCache( 13 | fn: AsyncFunction, 14 | options: CacheOptions, 15 | ...args: Params> 16 | ): Promise; 17 | 18 | async function withCache( 19 | fn: AsyncFunction, 20 | ...args: Params> 21 | ): Promise; 22 | 23 | async function withCache( 24 | fn: AsyncFunction, 25 | ...args: 26 | | Params> 27 | | [CacheOptions, ...Params>] 28 | ): Promise { 29 | let options: CacheOptions | undefined = undefined; 30 | let parameters: Params>; 31 | 32 | if (args.length > 0 && typeof args[0] === "object" && "ttl" in args[0]) { 33 | options = args.shift() as CacheOptions; 34 | parameters = args as Params>; 35 | } else { 36 | parameters = args as Params>; 37 | } 38 | 39 | let cacheKey = options?.key; 40 | 41 | if (cacheKey === undefined) { 42 | cacheKey = generateCacheKey(fn.name, ...parameters); 43 | } 44 | 45 | let cachedResult = getCache(cacheKey); 46 | 47 | if (cachedResult !== undefined) { 48 | return cachedResult; 49 | } 50 | 51 | const result = await fn(...parameters); 52 | setCache(cacheKey, result); 53 | return result; 54 | } 55 | 56 | export default withCache; 57 | -------------------------------------------------------------------------------- /packages/ui/src/api/client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getRestApiUrl } from "../config/config"; 3 | 4 | const apiClient = axios.create({ 5 | baseURL: getRestApiUrl(), 6 | headers: { 7 | "Content-type": "application/json", 8 | }, 9 | }); 10 | 11 | export default apiClient; 12 | -------------------------------------------------------------------------------- /packages/ui/src/api/nodes.ts: -------------------------------------------------------------------------------- 1 | import client from "./client"; 2 | 3 | export async function getNodeExtensions() { 4 | let response; 5 | try { 6 | response = await client.get(`/node/extensions`); 7 | } catch (error) { 8 | console.error("Error fetching configuration:", error); 9 | throw error; 10 | } 11 | return response.data?.extensions; 12 | } 13 | 14 | export async function getDynamicConfig(processorType: string, data: any) { 15 | let response; 16 | const dataToSend = { 17 | processorType, 18 | data, 19 | }; 20 | try { 21 | response = await client.post(`/node/extensions/dynamic`, dataToSend); 22 | } catch (error) { 23 | console.error("Error fetching configuration:", error); 24 | throw error; 25 | } 26 | return response.data; 27 | } 28 | 29 | export async function getModels(providerName: string) { 30 | let response; 31 | try { 32 | response = await client.get(`/node/openapi/${providerName}/models`); 33 | } catch (error) { 34 | console.error("Error fetching configuration:", error); 35 | throw error; 36 | } 37 | return response.data; 38 | } 39 | 40 | export async function getModelConfig(providerName: string, id: string) { 41 | let response; 42 | try { 43 | response = await client.get(`/node/openapi/${providerName}/config/${id}`); 44 | return response.data; 45 | } catch (error) { 46 | console.error("Error fetching configuration:", error); 47 | throw error; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/ui/src/api/parameters.ts: -------------------------------------------------------------------------------- 1 | import client from "./client"; 2 | 3 | export async function getParameters() { 4 | let response; 5 | try { 6 | response = await client.get(`/parameters`); 7 | } catch (error) { 8 | console.error("Error fetching configuration:", error); 9 | throw error; 10 | } 11 | return response.data; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/src/api/uploadFile.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosProgressEvent } from "axios"; 2 | import client from "./client"; 3 | 4 | export async function getUploadAndDownloadUrl(filename?: string) { 5 | try { 6 | const data = { filename }; 7 | const response = await client.get("/upload", { params: data }); 8 | return response.data; 9 | } catch (error) { 10 | console.error("Error while trying to get upload link :", error); 11 | throw error; 12 | } 13 | } 14 | 15 | export async function uploadWithS3Link(s3UploadData: any, file: File) { 16 | const config = { 17 | // headers: { 18 | // "Content-Type": "multipart/form-data", 19 | // }, 20 | onUploadProgress: (progressEvent: AxiosProgressEvent) => { 21 | if (!progressEvent.total) return; 22 | 23 | const percentCompleted = Math.round( 24 | (progressEvent.loaded * 100) / progressEvent.total, 25 | ); 26 | 27 | console.log(`Upload progress: ${percentCompleted}%`); 28 | }, 29 | }; 30 | 31 | try { 32 | const url = s3UploadData.url; 33 | const fields = s3UploadData.fields; 34 | 35 | const formData = new FormData(); 36 | 37 | Object.keys(fields).forEach((key) => { 38 | formData.append(key, fields[key]); 39 | }); 40 | 41 | formData.append("file", file); 42 | 43 | await axios.post(url, formData, config); 44 | } catch (error) { 45 | console.error("Error uploading file :", error); 46 | throw error; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/ui/src/components/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingScreenSpinner } from "./nodes/Node.styles"; 2 | 3 | const LoadingScreen = () => { 4 | return ( 5 |
10 |
11 | 12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default LoadingScreen; 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/bars/dnd-sidebar/GripIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export function GripIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/bars/dnd-sidebar/Section.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { NodeSection } from "../../../nodes-configuration/sectionConfig"; 3 | import { 4 | FiArrowDown, 5 | FiChevronDown, 6 | FiChevronRight, 7 | FiChevronUp, 8 | } from "react-icons/fi"; 9 | import { useState } from "react"; 10 | 11 | interface SidebarSectionProps { 12 | section: NodeSection; 13 | index: number; 14 | children: React.ReactNode; 15 | } 16 | 17 | function SidebarSection({ section, index, children }: SidebarSectionProps) { 18 | const { t } = useTranslation("flow"); 19 | const [show, setShow] = useState(true); 20 | 21 | function toggleShow() { 22 | setShow((prev) => !prev); 23 | } 24 | return ( 25 |
26 |
27 |

28 | {section.icon && } 29 | {t(section.label)} 30 |

31 | 32 | {show ? ( 33 | 37 | ) : ( 38 | 42 | )} 43 |
44 | 45 | {show && children} 46 |
47 | ); 48 | } 49 | 50 | export default SidebarSection; 51 | -------------------------------------------------------------------------------- /packages/ui/src/components/buttons/ButtonRunAll.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import { FaPlay, FaSpinner } from "react-icons/fa"; 3 | import { memo } from "react"; 4 | import TapScale from "../shared/motions/TapScale"; 5 | import { Tooltip } from "react-tooltip"; 6 | 7 | interface ButtonRunAllProps { 8 | onClick: () => void; 9 | isRunning: boolean; 10 | small?: boolean; 11 | } 12 | const ButtonRunAll: React.FC = ({ 13 | onClick, 14 | isRunning, 15 | small, 16 | }) => { 17 | return ( 18 | 19 | 36 | 37 | ); 38 | }; 39 | 40 | export default memo(ButtonRunAll); 41 | 42 | const spin = keyframes` 43 | 0% { transform: rotate(0deg); } 44 | 100% { transform: rotate(360deg); } 45 | `; 46 | 47 | const Spinner = styled(FaSpinner)` 48 | animation: ${spin} 1s linear infinite; 49 | `; 50 | -------------------------------------------------------------------------------- /packages/ui/src/components/buttons/ConfigurationButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { FiSettings } from "react-icons/fi"; 3 | import styled from "styled-components"; 4 | 5 | interface RightButtonProps { 6 | onClick: () => void; 7 | color?: string; 8 | icon?: React.ReactNode; 9 | text?: string; 10 | bottom?: string; 11 | } 12 | 13 | const RightIconButton: React.FC = ({ 14 | onClick, 15 | color = "#808080", 16 | icon = , 17 | bottom = "30px", 18 | }) => { 19 | return ( 20 | 26 |
{icon}
27 |
28 | ); 29 | }; 30 | 31 | const StyledRightButton = styled.div<{ color: string; bottom: string }>` 32 | bottom: ${(props) => props.bottom}; 33 | background-color: ${(props) => props.color}; 34 | `; 35 | 36 | export default memo(RightIconButton); 37 | -------------------------------------------------------------------------------- /packages/ui/src/components/buttons/DefaultSwitch.tsx: -------------------------------------------------------------------------------- 1 | import Switch from "react-switch"; 2 | 3 | interface DefaultSwitchProps { 4 | onChange: (value: boolean) => void; 5 | checked: boolean; 6 | } 7 | 8 | export default function DefaultSwitch({ 9 | onChange, 10 | checked, 11 | }: DefaultSwitchProps) { 12 | const handleChange = (value: boolean) => { 13 | onChange(value); 14 | }; 15 | 16 | return ( 17 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/components/buttons/EdgeTypeButton.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | export interface EdgeTypeIconEntry { 5 | src: string; 6 | edgeType: string; 7 | } 8 | 9 | interface EdgeTypeButtonProps { 10 | edgeType: string; 11 | onChangeEdgeType: (newEdgeType: string) => void; 12 | } 13 | 14 | export default function EdgeTypeButton({ 15 | edgeType, 16 | onChangeEdgeType, 17 | }: EdgeTypeButtonProps) { 18 | const { t } = useTranslation("flow"); 19 | 20 | const edgeTypeIconsMapping = useMemo( 21 | () => [ 22 | { src: `./curve-edge.svg`, edgeType: "default" }, 23 | { src: `./smooth-step-edge.svg`, edgeType: "smoothstep" }, 24 | { src: `./straight-edge.svg`, edgeType: "straight" }, 25 | { src: `./step-edge.svg`, edgeType: "step" }, 26 | ], 27 | [], 28 | ); 29 | 30 | const handleChangeEdgeType = () => { 31 | const currentIndex = edgeTypeIconsMapping.findIndex( 32 | (et) => et.edgeType === edgeType, 33 | ); 34 | const nextIndex = (currentIndex + 1) % edgeTypeIconsMapping.length; 35 | onChangeEdgeType(edgeTypeIconsMapping[nextIndex].edgeType); 36 | }; 37 | 38 | const currentEdgeType = edgeTypeIconsMapping.find( 39 | (et) => et.edgeType === edgeType, 40 | ); 41 | 42 | return ( 43 |
49 | {currentEdgeType?.edgeType} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /packages/ui/src/components/inputs/InputWithButton.tsx: -------------------------------------------------------------------------------- 1 | import NodeTextField from "../nodes/node-input/NodeTextField"; 2 | 3 | interface InputWithButtonProps { 4 | buttonText: string; 5 | onInputChange: (value: string) => void; 6 | onButtonClick: () => void; 7 | value: string; 8 | inputPlaceholder?: string; 9 | inputClassName?: string; 10 | buttonClassName?: string; 11 | } 12 | 13 | const InputWithButton = ({ 14 | inputPlaceholder, 15 | buttonText, 16 | value, 17 | onInputChange, 18 | onButtonClick, 19 | inputClassName = "", 20 | buttonClassName = "", 21 | }: InputWithButtonProps) => { 22 | return ( 23 |
24 |
25 | onInputChange(event.target.value)} 29 | value={value} 30 | /> 31 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default InputWithButton; 43 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/NodeHelpPopover.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Popover } from "@mantine/core"; 3 | import { NodeHelp, NodeHelpData } from "./utils/NodeHelp"; 4 | 5 | type NodeHelpPopoverProps = { 6 | children: React.ReactNode; 7 | showHelp: boolean; 8 | data: NodeHelpData; 9 | onClose: () => void; 10 | }; 11 | 12 | function NodeHelpPopover({ 13 | children, 14 | showHelp, 15 | data, 16 | onClose, 17 | }: NodeHelpPopoverProps) { 18 | return ( 19 | 36 | {children} 37 | 38 | {data && } 39 | 40 | 41 | ); 42 | } 43 | 44 | export default NodeHelpPopover; 45 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/node-input/ImageMaskCreatorFieldFlowAware.tsx: -------------------------------------------------------------------------------- 1 | import { getOutputExtension } from "../node-output/outputUtils"; 2 | import ImageMaskCreatorField from "./ImageMaskCreatorField"; 3 | 4 | interface ImageMaskCreatorFieldProps { 5 | onChange: (value: string) => void; 6 | } 7 | 8 | const extractImageUrls = (nodes: any[]) => { 9 | return nodes 10 | .flatMap((node) => { 11 | const outputData = node.data.outputData; 12 | if (typeof outputData === "string") return [outputData]; 13 | if (Array.isArray(outputData)) return outputData; 14 | return []; 15 | }) 16 | .filter((url) => getOutputExtension(url) === "imageUrl"); 17 | }; 18 | 19 | export default function ImageMaskCreatorFieldFlowAware({ 20 | onChange, 21 | }: ImageMaskCreatorFieldProps) { 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/node-input/TextAreaPopupWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from "@mantine/core"; 2 | import { FiExternalLink } from "react-icons/fi"; 3 | import { TextareaModal } from "../utils/TextareaModal"; 4 | import { useState } from "react"; 5 | 6 | interface TextAreaPopupWrapperProps { 7 | children: React.ReactNode; 8 | onChange: (value: string) => void; 9 | initValue: string; 10 | fieldName?: string; 11 | } 12 | 13 | function TextAreaPopupWrapper({ 14 | children, 15 | onChange, 16 | initValue, 17 | fieldName, 18 | }: TextAreaPopupWrapperProps) { 19 | const [modalOpen, setModalOpen] = useState(false); 20 | 21 | function openModal() { 22 | setModalOpen(true); 23 | } 24 | 25 | function closeModal() { 26 | setModalOpen(false); 27 | } 28 | 29 | return ( 30 | <> 31 |
32 | 33 | 34 | 35 | 36 | 37 | {children} 38 |
39 | {modalOpen && ( 40 | 46 | )} 47 | 48 | ); 49 | } 50 | 51 | export default TextAreaPopupWrapper; 52 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/node-output/PdfUrlOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FaDownload } from "react-icons/fa"; 3 | import styled from "styled-components"; 4 | import { getGeneratedFileName } from "./outputUtils"; 5 | 6 | interface PdfUrlOutputProps { 7 | url: string; 8 | name: string; 9 | } 10 | 11 | const PdfUrlOutput: React.FC = ({ url, name }) => { 12 | const handleDownloadClick = (event: React.MouseEvent) => { 13 | event.stopPropagation(); 14 | const link = document.createElement("a"); 15 | link.href = url; 16 | link.download = getGeneratedFileName(url, name); // Ensure getGeneratedFileName handles PDF filenames correctly 17 | link.target = "_blank"; 18 | link.click(); 19 | }; 20 | 21 | return ( 22 | 23 | 24 |

25 | Your browser does not support PDFs. Please download the PDF to view 26 | it: Download PDF. 27 |

28 |
29 |
33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | const OutputPdfContainer = styled.div` 40 | position: relative; 41 | margin-top: 10px; 42 | padding-top: 56.25%; // Maintain aspect ratio for PDF viewer 43 | height: 0; // Use padding to define height based on the container's width 44 | overflow: hidden; 45 | `; 46 | 47 | const OutputPdf = styled.object` 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | width: 100%; 52 | height: 100%; 53 | `; 54 | 55 | export default PdfUrlOutput; 56 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/node-output/VideoUrlOutput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { FaDownload } from "react-icons/fa"; 3 | import styled from "styled-components"; 4 | import { getGeneratedFileName } from "./outputUtils"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | interface VideoUrlOutputProps { 8 | url: string; 9 | name: string; 10 | } 11 | 12 | const VideoUrlOutput: React.FC = ({ url, name }) => { 13 | const { t } = useTranslation("flow"); 14 | const [hasError, setHasError] = useState(false); 15 | 16 | useEffect(() => { 17 | setHasError(false); 18 | }, [url]); 19 | 20 | const handleDownloadClick = (event: React.MouseEvent) => { 21 | event.stopPropagation(); 22 | const link = document.createElement("a"); 23 | link.href = url; 24 | link.download = getGeneratedFileName(url, name); 25 | link.target = "_blank"; 26 | link.click(); 27 | }; 28 | 29 | const handleError = () => { 30 | setHasError(true); 31 | }; 32 | 33 | const handleLoad = () => { 34 | setHasError(false); 35 | }; 36 | 37 | return ( 38 | 39 | {hasError ? ( 40 |

{t("ExpiredURL")}

41 | ) : ( 42 | <> 43 | {" "} 49 | {} 50 |
54 | 55 |
56 | 57 | )} 58 |
59 | ); 60 | }; 61 | 62 | const OutputVideoContainer = styled.div` 63 | position: relative; 64 | margin-top: 10px; 65 | `; 66 | 67 | const OutputVideo = styled.video` 68 | display: block; 69 | width: 100%; 70 | height: auto; 71 | border-radius: 8px; 72 | `; 73 | 74 | export default VideoUrlOutput; 75 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/node-output/outputUtils.ts: -------------------------------------------------------------------------------- 1 | import { OutputType } from "../../../nodes-configuration/types"; 2 | 3 | export const getFileExtension = (url: string) => { 4 | const extensionMatch = url.match(/\.([0-9a-z]+)(?:[\?#]|$)/i); 5 | return extensionMatch ? extensionMatch[1] : ""; 6 | }; 7 | 8 | export const getGeneratedFileName = (url: string, nodeName: string) => { 9 | const extension = getFileExtension(url); 10 | return `${nodeName}-output.${extension}`; 11 | }; 12 | 13 | const extensionToTypeMap: { [key: string]: OutputType } = { 14 | // Image extensions 15 | ".png": "imageUrl", 16 | ".jpg": "imageUrl", 17 | ".gif": "imageUrl", 18 | ".jpeg": "imageUrl", 19 | ".webp": "imageUrl", 20 | // Video extensions 21 | ".mp4": "videoUrl", 22 | ".mov": "videoUrl", 23 | // Audio extensions 24 | ".mp3": "audioUrl", 25 | ".wav": "audioUrl", 26 | // 3D extensions 27 | ".obj": "3dUrl", 28 | ".glb": "3dUrl", 29 | // Other extensions 30 | ".pdf": "fileUrl", 31 | ".txt": "fileUrl", 32 | }; 33 | 34 | export function getOutputExtension(output: string): OutputType { 35 | if (!output) return "markdown"; 36 | if (typeof output !== "string") return "markdown"; 37 | 38 | let extension = Object.keys(extensionToTypeMap).find((ext) => 39 | output.endsWith(ext), 40 | ); 41 | 42 | if (!extension) { 43 | extension = "." + getFileTypeFromUrl(output); 44 | } 45 | 46 | return extension ? extensionToTypeMap[extension] : "markdown"; 47 | } 48 | 49 | export function getFileTypeFromUrl(url: string) { 50 | const lastDotIndex = url.lastIndexOf("."); 51 | const urlWithoutParams = url.includes("?") 52 | ? url.substring(0, url.indexOf("?")) 53 | : url; 54 | const fileType = urlWithoutParams.substring(lastDotIndex + 1); 55 | return fileType; 56 | } 57 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/types/node.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig, NodeSubConfig } from "../../../nodes-configuration/types"; 2 | 3 | export interface NodeInput { 4 | inputName: string; 5 | inputNode: string; 6 | inputNodeOutputKey: number; 7 | } 8 | 9 | export interface NodeAppearance { 10 | color?: string; 11 | customName?: string; 12 | fontSize?: number; 13 | } 14 | 15 | export interface NodeData { 16 | id: string; 17 | name: string; 18 | handles: any; 19 | processorType: string; 20 | nbOutput: number; 21 | inputs: NodeInput[]; 22 | outputData?: string[] | string; 23 | lastRun?: string; 24 | missingFields?: string[]; 25 | config: NodeConfig; 26 | appearance?: NodeAppearance; 27 | variantConfig?: NodeSubConfig; 28 | [key: string]: any; 29 | } 30 | 31 | export interface GenericNodeData extends NodeData { 32 | width?: number; 33 | height?: number; 34 | [key: string]: any; 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/utils/HintComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | interface HintComponentProps { 5 | hintId: string; 6 | textVar: string; 7 | } 8 | 9 | const HintComponent: React.FC = ({ hintId, textVar }) => { 10 | const { t } = useTranslation("flow"); 11 | const [showHint, setShowHint] = useState(false); 12 | 13 | useEffect(() => { 14 | const storageKey = `hasHintBeenHidden-${hintId}`; 15 | const hasHintBeenHidden = localStorage.getItem(storageKey); 16 | if (hasHintBeenHidden) { 17 | setShowHint(false); 18 | } else { 19 | setShowHint(true); 20 | } 21 | }, [hintId]); 22 | 23 | const handleHideClick = () => { 24 | const storageKey = `hasHintBeenHidden-${hintId}`; 25 | localStorage.setItem(storageKey, "true"); 26 | setShowHint(false); 27 | }; 28 | 29 | return ( 30 | <> 31 | {showHint && ( 32 |
33 |
{t(textVar)}
34 | 40 |
41 | )} 42 | 43 | ); 44 | }; 45 | 46 | export default HintComponent; 47 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/utils/ImageModal.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import { FaTimes } from "react-icons/fa"; 3 | 4 | interface ImageModalProps { 5 | src: string; 6 | alt: string; 7 | onClose: () => void; 8 | } 9 | export function ImageModal({ src, alt, onClose }: ImageModalProps) { 10 | return ReactDOM.createPortal( 11 | <> 12 |
18 |
19 | {alt} 20 | 27 |
28 |
29 | , 30 | document.body, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/components/nodes/utils/ImageZoomable.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { FaSearchPlus, FaTimes } from "react-icons/fa"; 3 | import { ImageModal } from "./ImageModal"; 4 | 5 | interface ImageZoomableProps { 6 | src: string; 7 | alt: string; 8 | } 9 | 10 | export function ImageZoomable({ src, alt }: ImageZoomableProps) { 11 | const [isImageZoomed, setImageZoomed] = useState(false); 12 | 13 | const handleImageZoom = () => setImageZoomed(true); 14 | const handleCloseZoom = () => setImageZoomed(false); 15 | return ( 16 | <> 17 |
18 | {alt} 19 | 26 |
27 | {isImageZoomed && ( 28 | 29 | )} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/components/players/VideoJS.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | import "video.js/dist/video-js.css"; 4 | import "videojs-wavesurfer/dist/css/videojs.wavesurfer.css"; 5 | 6 | import videojs from "video.js"; 7 | import "videojs-wavesurfer"; 8 | 9 | interface VideoJSProps { 10 | options: any; 11 | onReady?: (player: any) => void; 12 | } 13 | 14 | export const VideoJS = (props: VideoJSProps) => { 15 | const videoRef = React.useRef(null); 16 | const playerRef = React.useRef(null); 17 | const { options, onReady } = props; 18 | 19 | useEffect(() => { 20 | if (!playerRef.current) { 21 | const videoElement = document.createElement("video-js"); 22 | 23 | videoElement.classList.add("vjs-big-play-centered"); 24 | 25 | if (!videoRef.current) return; 26 | 27 | videoRef.current.appendChild(videoElement); 28 | 29 | const player = (playerRef.current = videojs(videoElement, options, () => { 30 | onReady && onReady(player); 31 | })); 32 | } else { 33 | const player = playerRef.current; 34 | 35 | player.autoplay(options.autoplay); 36 | player.src(options.sources); 37 | } 38 | }, [options]); 39 | 40 | useEffect(() => { 41 | const player = playerRef.current; 42 | 43 | return () => { 44 | if (player && !player.isDisposed()) { 45 | player.dispose(); 46 | playerRef.current = null; 47 | } 48 | }; 49 | }, [playerRef]); 50 | 51 | return ( 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default VideoJS; 59 | -------------------------------------------------------------------------------- /packages/ui/src/components/popups/DefaultPopup.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import EaseOut from "../shared/motions/EaseOut"; 4 | 5 | interface DefaultPopupWrapperProps { 6 | show: boolean; 7 | onClose: () => void; 8 | centered?: boolean; 9 | popupClassNames?: string; 10 | style?: CSSProperties; 11 | children: React.ReactNode; 12 | } 13 | 14 | export default function DefaultPopupWrapper({ 15 | show, 16 | onClose, 17 | centered, 18 | popupClassNames, 19 | style, 20 | children, 21 | }: DefaultPopupWrapperProps) { 22 | if (!show) return null; 23 | 24 | return ReactDOM.createPortal( 25 |
30 |
{ 33 | e.stopPropagation(); 34 | }} 35 | onTouchEnd={(e) => e.stopPropagation()} 36 | style={{ ...style }} 37 | > 38 | {children} 39 |
40 |
, 41 | document.body, 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/ui/src/components/popups/UserMessagePopup.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from "@mantine/core"; 2 | import { ReactNode } from "react"; 3 | 4 | export enum MessageType { 5 | Error, 6 | Info, 7 | } 8 | 9 | export interface UserMessage { 10 | type?: MessageType; 11 | nodeId?: string; 12 | content: string; 13 | } 14 | 15 | interface PopupProps { 16 | isOpen: boolean; 17 | message: UserMessage; 18 | children?: ReactNode; 19 | onClose: () => void; 20 | } 21 | 22 | function UserMessagePopup(props: PopupProps) { 23 | return props.isOpen ? ( 24 | 39 |
40 | {!!props.message.nodeId && ( 41 |
42 |
43 | {props.message.nodeId} 44 |
45 |
46 | )} 47 |
{props.message?.content}
48 | {props.children} 49 |
50 |
51 | ) : ( 52 | <> 53 | ); 54 | } 55 | 56 | export default UserMessagePopup; 57 | -------------------------------------------------------------------------------- /packages/ui/src/components/popups/config-popup/configMetadata.ts: -------------------------------------------------------------------------------- 1 | // src/configMetadata.ts 2 | export interface FieldMetadata { 3 | label: string; 4 | description: string; 5 | type: string; // e.g., "text", "password" 6 | required?: boolean; 7 | } 8 | 9 | export interface ConfigMetadata { 10 | [key: string]: FieldMetadata; 11 | } 12 | 13 | export interface AppConfig { 14 | S3_BUCKET_NAME: string; 15 | S3_AWS_ACCESS_KEY_ID: string; 16 | S3_AWS_SECRET_ACCESS_KEY: string; 17 | S3_AWS_REGION_NAME: string; 18 | S3_ENDPOINT_URL: string; 19 | REPLICATE_API_KEY: string; 20 | } 21 | 22 | export const configMetadata: ConfigMetadata = { 23 | S3_BUCKET_NAME: { 24 | label: "S3 Bucket Name", 25 | description: "The name of your S3-compatible storage bucket.", 26 | type: "text", 27 | }, 28 | S3_AWS_ACCESS_KEY_ID: { 29 | label: "S3 Access Key", 30 | description: "Your S3 access key.", 31 | type: "password", 32 | }, 33 | S3_AWS_SECRET_ACCESS_KEY: { 34 | label: "S3 Secret Access Key", 35 | description: "Your S3 secret access key.", 36 | type: "password", 37 | }, 38 | S3_AWS_REGION_NAME: { 39 | label: "S3 AWS Region Name", 40 | description: "The AWS region where your S3 bucket is located.", 41 | type: "text", 42 | }, 43 | S3_ENDPOINT_URL: { 44 | label: "Optional - S3 Endpoint URL", 45 | description: "The URL of your S3-compatible storage endpoint.", 46 | type: "text", 47 | }, 48 | REPLICATE_API_KEY: { 49 | label: "Replicate API Key", 50 | description: "Used to fetch Replicate models.", 51 | type: "password", 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /packages/ui/src/components/popups/shared/FilterGrid.tsx: -------------------------------------------------------------------------------- 1 | export type FilterItem = { 2 | name: string; 3 | slug: string; 4 | }; 5 | 6 | type FilterGridProps = { 7 | filters: FilterItem[]; 8 | selectedFilter: string; 9 | onSelectFilter: (slug: string) => void; 10 | }; 11 | 12 | function FilterGrid({ 13 | filters, 14 | selectedFilter, 15 | onSelectFilter, 16 | }: FilterGridProps) { 17 | function getUpperCaseFirstCharString(value: string) { 18 | return value.charAt(0).toUpperCase() + value.slice(1); 19 | } 20 | return ( 21 |
22 | {filters && 23 | filters.map((filter) => ( 24 |
30 |

onSelectFilter(filter.slug)} 33 | > 34 | {getUpperCaseFirstCharString(filter.name)} 35 |

36 |
37 | ))} 38 |
39 | ); 40 | } 41 | 42 | export default FilterGrid; 43 | -------------------------------------------------------------------------------- /packages/ui/src/components/popups/shared/Grid.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | interface GridProps { 4 | items: T[]; 5 | renderItem: (item: T, onValidate: (id: string) => void) => JSX.Element; 6 | onValidate: (id: string) => void; 7 | numberColMax?: number; 8 | } 9 | 10 | const getGridTemplateColumns = (maxCols: number) => { 11 | return ` 12 | grid-template-columns: repeat(1, minmax(0, 1fr)); 13 | @media (min-width: 640px) { 14 | grid-template-columns: repeat(${Math.min(2, maxCols)}, minmax(0, 1fr)); 15 | } 16 | @media (min-width: 768px) { 17 | grid-template-columns: repeat(${Math.min(3, maxCols)}, minmax(0, 1fr)); 18 | } 19 | @media (min-width: 1024px) { 20 | grid-template-columns: repeat(${Math.min(4, maxCols)}, minmax(0, 1fr)); 21 | } 22 | @media (min-width: 1280px) { 23 | grid-template-columns: repeat(${maxCols}, minmax(0, 1fr)); 24 | } 25 | `; 26 | }; 27 | 28 | export default function Grid({ 29 | items, 30 | onValidate, 31 | renderItem, 32 | numberColMax = 2, 33 | }: GridProps) { 34 | return ( 35 | 36 | {items && items.map((item) => renderItem(item, onValidate))} 37 | 38 | ); 39 | } 40 | 41 | const StyledGrid = styled.div<{ maxCols: number }>` 42 | display: grid; 43 | width: 100%; 44 | gap: 1rem; 45 | ${({ maxCols }) => getGridTemplateColumns(maxCols)} 46 | `; 47 | -------------------------------------------------------------------------------- /packages/ui/src/components/popups/shared/LoadMoreButton.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { LoadingIcon } from "../../nodes/Node.styles"; 3 | 4 | interface LoadMoreButtonProps { 5 | loading: boolean; 6 | cursor: string | null; 7 | onLoadMore: () => void; 8 | } 9 | 10 | export default function LoadMoreButton({ 11 | loading, 12 | cursor, 13 | onLoadMore, 14 | }: LoadMoreButtonProps) { 15 | const { t } = useTranslation("flow"); 16 | return ( 17 |
18 | {loading ? ( 19 | 20 | ) : ( 21 | cursor != null && 22 | cursor != "" && ( 23 |
27 | {t("LoadMore")} 28 |
29 | ) 30 | )} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/src/components/selectors/ActionGroup.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface ActionGroupProps { 4 | actions: Action[]; 5 | showIcon: boolean; 6 | } 7 | 8 | export interface Action { 9 | name: string; 10 | icon: ReactNode; 11 | value: T; 12 | onClick: () => void; 13 | hoverColor?: string; 14 | tooltipPosition?: "top" | "bottom" | "left" | "right"; 15 | } 16 | 17 | export default function ActionGroup({ 18 | actions: options, 19 | showIcon, 20 | }: ActionGroupProps) { 21 | return ( 22 |
26 | {options.map((option) => { 27 | return ( 28 | 46 | ); 47 | })} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/ui/src/components/selectors/ColorSelector.tsx: -------------------------------------------------------------------------------- 1 | const colorList = [ 2 | "transparent", 3 | "chocolate", 4 | "firebrick", 5 | "cyan", 6 | "greenyellow", 7 | "gold", 8 | "blueviolet", 9 | "magenta", 10 | ]; 11 | 12 | interface ColorSelectorProps { 13 | onChangeColor: (color: string) => void; 14 | } 15 | 16 | export default function ColorSelector({ onChangeColor }: ColorSelectorProps) { 17 | return ( 18 | <> 19 | {colorList.map((color, index) => ( 20 |
onChangeColor(color)} 27 | onTouchStart={() => onChangeColor(color)} 28 | /> 29 | ))} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/components/selectors/ExpandableBloc.tsx: -------------------------------------------------------------------------------- 1 | import { Disclosure, Transition } from "@headlessui/react"; 2 | import { FiChevronRight } from "react-icons/fi"; 3 | 4 | interface ExpandableBlocProps { 5 | title: string; 6 | children: React.ReactNode; 7 | defaultOpen?: boolean; 8 | } 9 | 10 | export default function ExpandableBloc({ 11 | title, 12 | defaultOpen, 13 | children, 14 | }: ExpandableBlocProps) { 15 | return ( 16 | 17 | {({ open }) => ( 18 | <> 19 | 20 | 21 |
{title}
22 |
23 | 31 | {children} 32 | 33 | 34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/ui/src/components/selectors/OptionSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | interface OptionSelectorProps { 4 | options: Option[]; 5 | selectedOption?: T; 6 | onSelectOption: (option: Option) => void; 7 | showLabels?: boolean; 8 | } 9 | 10 | export interface Option { 11 | name: string; 12 | icon: ReactNode; 13 | value: T; 14 | } 15 | 16 | export default function OptionSelector({ 17 | options, 18 | selectedOption, 19 | onSelectOption, 20 | showLabels, 21 | }: OptionSelectorProps) { 22 | return ( 23 |
24 | {options.map((option) => { 25 | const isSelected = selectedOption === option.value; 26 | return ( 27 |
onSelectOption(option)} 38 | > 39 | {option.icon} 40 | {showLabels && {option.name}} 41 |
42 | ); 43 | })} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /packages/ui/src/components/shared/motions/EaseOut.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { AnimationProps } from "./types"; 3 | 4 | function EaseOut({ children }: AnimationProps) { 5 | return ( 6 | 16 | {children} 17 | 18 | ); 19 | } 20 | 21 | export default EaseOut; 22 | -------------------------------------------------------------------------------- /packages/ui/src/components/shared/motions/TapScale.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion" 2 | import { AnimationProps } from "./types"; 3 | import { memo } from "react"; 4 | 5 | interface TapScaleProps extends AnimationProps { 6 | scale?: number; 7 | } 8 | 9 | function TapScale({ children, scale }: TapScaleProps) { 10 | const actualScale = scale ?? 0.9 11 | return ( 12 | 15 | {children} 16 | 17 | ) 18 | } 19 | 20 | export default memo(TapScale); -------------------------------------------------------------------------------- /packages/ui/src/components/shared/motions/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | 4 | export interface AnimationProps { 5 | children: ReactNode; 6 | } -------------------------------------------------------------------------------- /packages/ui/src/components/tools/Fallback.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | const rotate = keyframes` 5 | 0% { 6 | transform: rotate(0deg); 7 | } 8 | 100% { 9 | transform: rotate(360deg); 10 | } 11 | `; 12 | 13 | const LoadingSpinner = styled.div` 14 | display: inline-block; 15 | width: 50px; 16 | height: 50px; 17 | border: 3px solid ${({ theme }) => theme.accent}; 18 | border-radius: 50%; 19 | border-top-color: transparent; 20 | animation: ${rotate} 1s linear infinite; 21 | `; 22 | 23 | const LoadingScreen = styled.div` 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | height: 100vh; 28 | width: 100vw; 29 | background-color: ${({ theme }) => theme.bg}; 30 | `; 31 | 32 | export const Fallback = () => ( 33 | 34 | 35 | 36 | ); -------------------------------------------------------------------------------- /packages/ui/src/config/config.ts: -------------------------------------------------------------------------------- 1 | const HOST = import.meta.env.VITE_APP_WS_HOST || "localhost"; 2 | const WS_PORT = import.meta.env.VITE_APP_WS_PORT || 5000; 3 | const REST_API_PORT = import.meta.env.VITE_APP_API_REST_PORT || 5000; 4 | const USE_HTTPS = import.meta.env.VITE_APP_USE_HTTPS || "false"; 5 | const USE_CACHE = import.meta.env.VITE_APP_USE_CACHE?.toLowerCase() || "true"; 6 | const CURRENT_APP_VERSION = import.meta.env.VITE_APP_VERSION; 7 | const DEFAULT_NODES_HIDDEN_LIST = 8 | import.meta.env.VITE_APP_DEFAULT_NODES_HIDDEN_LIST || ""; 9 | const IS_DEV = import.meta.env.VITE_APP_IS_DEV?.toLowerCase() === "true"; 10 | const protocol = USE_HTTPS.toLowerCase() === "true" ? "https" : "http"; 11 | 12 | export const getWsUrl = () => `${protocol}://${HOST}:${WS_PORT}`; 13 | export const getRestApiUrl = () => `${protocol}://${HOST}:${REST_API_PORT}`; 14 | export const isCacheEnabled = () => USE_CACHE === "true"; 15 | export const getCurrentAppVersion = () => CURRENT_APP_VERSION; 16 | export const getDefaultNodesHiddenList = () => 17 | DEFAULT_NODES_HIDDEN_LIST.split(",") as string[]; 18 | 19 | export const isDev = () => IS_DEV; 20 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useFlowSocketListeners.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { SocketContext } from "../providers/SocketProvider"; 3 | import { useTranslation } from "react-i18next"; 4 | import { toastInfoMessage } from "../utils/toastUtils"; 5 | 6 | export const useSocketListeners = < 7 | ProgressData, 8 | ErrorData, 9 | CurrentNodeRunningData, 10 | >( 11 | onProgress: (data: ProgressData) => void, 12 | onError: (data: ErrorData) => void, 13 | onRunEnd: () => void, 14 | onCurrentNodeRunning: (data: CurrentNodeRunningData) => void, 15 | onDisconnect?: (reason: string) => void, 16 | ) => { 17 | const { t } = useTranslation("flow"); 18 | const { socket } = useContext(SocketContext); 19 | 20 | useEffect(() => { 21 | if (socket) { 22 | socket.on("progress", onProgress); 23 | socket.on("error", onError); 24 | socket.on("run_end", onRunEnd); 25 | socket.on("current_node_running", onCurrentNodeRunning); 26 | socket.on( 27 | "disconnect", 28 | onDisconnect ? onDisconnect : defaultOnDisconnect, 29 | ); 30 | } 31 | 32 | return () => { 33 | if (socket) { 34 | socket.off("progress", onProgress); 35 | socket.off("error", onError); 36 | socket.off("run_end", onRunEnd); 37 | socket.off("current_node_running", onCurrentNodeRunning); 38 | socket.off( 39 | "disconnect", 40 | onDisconnect ? onDisconnect : defaultOnDisconnect, 41 | ); 42 | } 43 | }; 44 | }, [socket]); 45 | 46 | function defaultOnDisconnect(reason: string) { 47 | if (reason === "transport close") { 48 | toastInfoMessage(t("socketConnectionLost"), "socket-connection-lost"); 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useHandlePositions.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { generateIdForHandle } from "../utils/flowUtils"; 3 | import { LinkedHandlePositions } from "../components/handles/HandleWrapper"; 4 | import { Position } from "reactflow"; 5 | 6 | const useHandlePositions = ( 7 | data: any, 8 | nbInput: number, 9 | outputHandleIds: string[], 10 | ) => { 11 | const allInputHandleIds = Array.from({ length: nbInput }, (_, i) => i).map( 12 | (index) => generateIdForHandle(index), 13 | ); 14 | 15 | const allHandleIds = useMemo(() => { 16 | const inputHandleIds = Array.from({ length: nbInput }, (_, i) => i).map( 17 | (index) => generateIdForHandle(index), 18 | ); 19 | return [...inputHandleIds, ...outputHandleIds]; 20 | }, [nbInput, outputHandleIds]); 21 | 22 | const allHandlePositions = useMemo(() => { 23 | const positions = {} as LinkedHandlePositions; 24 | allHandleIds.forEach((id) => { 25 | let currentPosition: Position = 26 | data?.handles?.[id] ?? 27 | (id.includes("out") ? Position.Right : Position.Left); 28 | positions[currentPosition] = [...(positions[currentPosition] || []), id]; 29 | }); 30 | return positions; 31 | }, [allHandleIds, data]); 32 | 33 | return { 34 | nbInput, 35 | allInputHandleIds, 36 | allHandleIds, 37 | allHandlePositions, 38 | }; 39 | }; 40 | 41 | export default useHandlePositions; 42 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useHandleShowOutput.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect } from "react"; 2 | 3 | interface UseHandleShowOutputProps { 4 | showOnlyOutput?: boolean; 5 | setCollapsed: Dispatch>; 6 | setShowLogs?: Dispatch>; 7 | } 8 | 9 | const useHandleShowOutput = ({ 10 | showOnlyOutput, 11 | setCollapsed, 12 | setShowLogs, 13 | }: UseHandleShowOutputProps) => { 14 | useEffect(() => { 15 | if (showOnlyOutput !== undefined) { 16 | setCollapsed(showOnlyOutput); 17 | if (setShowLogs !== undefined && showOnlyOutput) { 18 | setShowLogs(showOnlyOutput); 19 | } 20 | } 21 | }, [showOnlyOutput, setCollapsed, setShowLogs]); 22 | }; 23 | 24 | export default useHandleShowOutput; 25 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useIsPlaying.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { NodeContext } from "../providers/NodeProvider"; 3 | 4 | /** 5 | * This hook stop playing animation whenever an error is raised globaly. 6 | */ 7 | export const useIsPlaying = (): [ 8 | boolean, 9 | React.Dispatch>, 10 | ] => { 11 | const { errorCount } = useContext(NodeContext); 12 | const [isPlaying, setIsPlaying] = useState(false); 13 | 14 | useEffect(() => { 15 | setIsPlaying(false); 16 | }, [errorCount]); 17 | 18 | return [isPlaying, setIsPlaying]; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useIsTouchDevice.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const useIsTouchDevice = (): boolean => { 4 | const [isTouchDevice, setIsTouchDevice] = useState(false); 5 | 6 | useEffect(() => { 7 | const checkTouchDevice = () => { 8 | setIsTouchDevice(window.matchMedia("(pointer: coarse)").matches); 9 | }; 10 | 11 | checkTouchDevice(); 12 | window.addEventListener("resize", checkTouchDevice); 13 | 14 | return () => { 15 | window.removeEventListener("resize", checkTouchDevice); 16 | }; 17 | }, []); 18 | 19 | return isTouchDevice; 20 | }; 21 | 22 | export default useIsTouchDevice; 23 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useLoading.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | type AsyncFunction = (...args: T) => Promise; 4 | 5 | type Params = T extends (...args: infer U) => any ? U : never; 6 | 7 | type StartLoadingWith = ( 8 | func: AsyncFunction, 9 | ...args: Params> 10 | ) => Promise; 11 | 12 | export const useLoading = (): [ 13 | isLoading: boolean, 14 | startLoadingWith: StartLoadingWith, 15 | ] => { 16 | const [isLoading, setIsLoading] = useState(false); 17 | 18 | const startLoadingWith: StartLoadingWith = async (func, ...args) => { 19 | setIsLoading(true); 20 | try { 21 | const result = await func(...args); 22 | setIsLoading(false); 23 | return result; 24 | } catch (error) { 25 | setIsLoading(false); 26 | throw error; 27 | } 28 | }; 29 | 30 | return [isLoading, startLoadingWith]; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | // useLocalStorage.ts 2 | import { useState, useEffect } from "react"; 3 | 4 | /** 5 | * A custom hook that synchronizes state with localStorage. 6 | * 7 | * @param key - The key under which the value is stored in localStorage. 8 | * @param initialValue - The initial value to use if the key does not exist in localStorage. 9 | * @returns A stateful value and a function to update it. 10 | */ 11 | function useLocalStorage( 12 | key: string, 13 | initialValue: T, 14 | ): [T, React.Dispatch>] { 15 | // Initialize state with a function to avoid reading localStorage on every render 16 | const [storedValue, setStoredValue] = useState(() => { 17 | if (typeof window === "undefined") { 18 | // If window is undefined, likely during SSR, return initialValue 19 | return initialValue; 20 | } 21 | try { 22 | const item = window.localStorage.getItem(key); 23 | return item ? (JSON.parse(item) as T) : initialValue; 24 | } catch (error) { 25 | console.warn(`Error reading localStorage key "${key}":`, error); 26 | return initialValue; 27 | } 28 | }); 29 | 30 | // useEffect to update localStorage whenever storedValue changes 31 | useEffect(() => { 32 | if (typeof window === "undefined") { 33 | // If window is undefined, do nothing 34 | return; 35 | } 36 | try { 37 | const valueToStore = 38 | storedValue instanceof Function 39 | ? storedValue(storedValue) 40 | : storedValue; 41 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 42 | } catch (error) { 43 | console.warn(`Error setting localStorage key "${key}":`, error); 44 | } 45 | }, [key, storedValue]); 46 | 47 | return [storedValue, setStoredValue]; 48 | } 49 | 50 | export default useLocalStorage; 51 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useRefreshOnAppearanceChange.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useRefreshOnAppearanceChange = ( 4 | updateNodeInternals: (id: string) => void, 5 | id: string, 6 | deps: any[], 7 | ) => { 8 | useEffect(() => { 9 | updateNodeInternals(id); 10 | }, deps); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ui/src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import Backend from "i18next-http-backend"; 4 | import LanguageDetector from "i18next-browser-languagedetector"; 5 | 6 | i18n 7 | .use(Backend) // load translation using http -> see /public/locales 8 | .use(LanguageDetector) 9 | .use(initReactI18next) 10 | .init({ 11 | load: "languageOnly", 12 | fallbackLng: "en", 13 | debug: false, 14 | interpolation: { 15 | escapeValue: false, 16 | }, 17 | backend: { 18 | loadPath: "/locales/{{lng}}/{{ns}}.json", 19 | }, 20 | }); 21 | 22 | export default i18n; 23 | -------------------------------------------------------------------------------- /packages/ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | html, 7 | body, 8 | #root { 9 | width: 100%; 10 | margin: 0; 11 | padding: 0; 12 | box-sizing: border-box; 13 | font-family: sans-serif; 14 | line-height: 1.5em; 15 | word-spacing: 0.16em; 16 | /*letter-spacing: 0.12em;*/ 17 | } 18 | 19 | #webpack-dev-server-client-overlay{ 20 | display: none; 21 | } 22 | 23 | html, body{ 24 | background: linear-gradient(0deg, hsl(180deg 9.33% 12.39%) 0%, hsl(0deg 0% 10.39%) 100%); 25 | } 26 | 27 | :root { 28 | --scrollbar-track-color: #F1F1F1; 29 | --scrollbar-thumb-color: #AAA; 30 | --scrollbar-border-color: #F1F1F1; 31 | --scrollbar-thumb-hover-color: #888; 32 | } 33 | 34 | :root .smart-view { 35 | --separator-border: #ffffff00; 36 | --sash-size: 8px; 37 | } 38 | 39 | body.dark-theme { 40 | --scrollbar-track-color: #2A2A2A; 41 | --scrollbar-thumb-color: rgba(85, 85, 85, 0.431); 42 | --scrollbar-border-color: #2A2A2A; 43 | --scrollbar-thumb-hover-color: #666; 44 | } 45 | 46 | 47 | /* Custom Slider Styles */ 48 | ::-webkit-scrollbar { 49 | width: 5px; 50 | height: 5px; 51 | } 52 | 53 | 54 | ::-webkit-scrollbar-track { 55 | background-color: var(--scrollbar-track-color); 56 | } 57 | 58 | ::-webkit-scrollbar-thumb { 59 | background-color: var(--scrollbar-thumb-color); 60 | border-radius: 5px; 61 | border: 2px solid var(--scrollbar-border-color); 62 | } 63 | 64 | ::-webkit-scrollbar-thumb:hover { 65 | background-color: var(--scrollbar-thumb-hover-color); 66 | } 67 | 68 | 69 | /* Custom React Flow Handle Style */ 70 | .custom-handle.react-flow__handle { 71 | top: initial; 72 | left: initial; 73 | transform: initial; 74 | } 75 | 76 | 77 | .react-grid-placeholder { 78 | background-color: #21FEE5 !important; 79 | } 80 | 81 | .markdown-body { 82 | color: #f5f5f5 !important; 83 | } 84 | 85 | .markdown-body pre { 86 | background-color: transparent !important; 87 | } 88 | 89 | 90 | @import 'tailwindcss/base'; 91 | @import 'tailwindcss/components'; 92 | @import 'tailwindcss/utilities'; -------------------------------------------------------------------------------- /packages/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./init"; 2 | 3 | import React, { Suspense } from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | import "./index.css"; 6 | import "@mantine/core/styles.css"; 7 | import { createTheme, MantineProvider } from "@mantine/core"; 8 | import "react-toastify/dist/ReactToastify.css"; 9 | import reportWebVitals from "./reportWebVitals"; 10 | import { ThemeProvider } from "./providers/ThemeProvider"; 11 | import { GlobalStyle } from "./components/nodes/Node.styles"; 12 | import { Fallback } from "./components/tools/Fallback"; 13 | import "./i18n"; 14 | import { ToastContainer } from "react-toastify"; 15 | import Main from "./Main"; 16 | 17 | const root = ReactDOM.createRoot( 18 | document.getElementById("root") as HTMLElement, 19 | ); 20 | 21 | const theme = createTheme({}); 22 | 23 | root.render( 24 | <> 25 | 26 | 27 | 28 | }> 29 | 30 |
31 | 32 | 33 | 34 | , 35 | ); 36 | 37 | // If you want to start measuring performance in your app, pass a function 38 | // to log results (for example: reportWebVitals(console.log)) 39 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 40 | reportWebVitals(); 41 | -------------------------------------------------------------------------------- /packages/ui/src/init.js: -------------------------------------------------------------------------------- 1 | // init.js 2 | window.global ||= window; 3 | -------------------------------------------------------------------------------- /packages/ui/src/layout/main-layout/wrapper/FlowErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, ErrorInfo } from "react"; 2 | 3 | interface ErrorBoundaryProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | interface ErrorBoundaryState { 8 | hasError: boolean; 9 | } 10 | 11 | export default function ErrorBoundary({ children }: ErrorBoundaryProps) { 12 | const [state, setState] = useState({ hasError: false }); 13 | 14 | useEffect(() => { 15 | const handleError = (error: Error, errorInfo: ErrorInfo) => { 16 | if (!error) return; 17 | if (error.stack?.includes("videojs-wavesurfer")) return; //TMP - But no idea how to catch them otherwise... 18 | console.error("Error Boundary caught an error", error, errorInfo); 19 | setState({ hasError: true }); 20 | }; 21 | 22 | const globalErrorHandler = (event: ErrorEvent) => { 23 | handleError(event.error, { componentStack: "" }); 24 | }; 25 | 26 | const unhandledRejectionHandler = (event: PromiseRejectionEvent) => { 27 | handleError(event.reason, { componentStack: "" }); 28 | }; 29 | 30 | window.addEventListener("error", globalErrorHandler); 31 | window.addEventListener("unhandledrejection", unhandledRejectionHandler); 32 | 33 | return () => { 34 | window.removeEventListener("error", globalErrorHandler); 35 | window.removeEventListener( 36 | "unhandledrejection", 37 | unhandledRejectionHandler, 38 | ); 39 | }; 40 | }, []); 41 | 42 | if (state.hasError) { 43 | return ( 44 |
45 |

Something went wrong with this flow.

46 | 47 |
48 | ); 49 | } 50 | return <>{children}; 51 | } 52 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/dallENode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | const dallENodeConfig: NodeConfig = { 4 | nodeName: "DALL-E 3", 5 | processorType: "dalle-prompt", 6 | icon: "OpenAILogo", 7 | hasInputHandle: true, 8 | fields: [ 9 | { 10 | type: "textarea", 11 | name: "prompt", 12 | placeholder: "DallEPromptPlaceholder", 13 | hideIfParent: true, 14 | }, 15 | { 16 | type: "select", 17 | name: "size", 18 | options: [ 19 | { 20 | label: "1024x1024", 21 | value: "1024x1024", 22 | default: true, 23 | }, 24 | { 25 | label: "1024x1792", 26 | value: "1024x1792", 27 | }, 28 | { 29 | label: "1792x1024", 30 | value: "1792x1024", 31 | }, 32 | ], 33 | }, 34 | { 35 | type: "select", 36 | name: "quality", 37 | options: [ 38 | { 39 | label: "standard", 40 | value: "standard", 41 | default: true, 42 | }, 43 | { 44 | label: "hd", 45 | value: "hd", 46 | }, 47 | ], 48 | }, 49 | ], 50 | outputType: "imageUrl", 51 | section: "models", 52 | helpMessage: "dallePromptHelp", 53 | }; 54 | 55 | export default dallENodeConfig; 56 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/gptVisionNode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | export const gptVisionNodeConfig: NodeConfig = { 4 | nodeName: "GPT Vision", 5 | processorType: "gpt-vision", 6 | icon: "OpenAILogo", 7 | inputNames: ["image_url", "prompt"], 8 | fields: [ 9 | { 10 | name: "prompt", 11 | label: "Prompt", 12 | type: "textarea", 13 | required: true, 14 | hasHandle: true, 15 | placeholder: "VisionPromptPlaceholder", 16 | }, 17 | { 18 | name: "image_url", 19 | label: "Image URL", 20 | type: "fileUpload", 21 | hasHandle: true, 22 | required: true, 23 | placeholder: "VisionImageURLPlaceholder", 24 | canAddChildrenFields: true, 25 | }, 26 | ], 27 | outputType: "markdown", 28 | hasInputHandle: true, 29 | section: "models", 30 | helpMessage: "gptVisionPromptHelp", 31 | showHandlesNames: true, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/inputTextNode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | const inputTextNodeConfig: NodeConfig = { 4 | nodeName: "Text", 5 | processorType: "input-text", 6 | icon: "AiOutlineEdit", 7 | fields: [ 8 | { 9 | type: "textarea", 10 | name: "inputText", 11 | required: true, 12 | placeholder: "InputPlaceholder", 13 | }, 14 | ], 15 | outputType: "text", 16 | defaultHideOutput: true, 17 | section: "input", 18 | helpMessage: "inputHelp", 19 | }; 20 | 21 | export default inputTextNodeConfig; 22 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/mergerPromptNode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | export const mergerPromptNode: NodeConfig = { 4 | nodeName: "MergerNode", 5 | processorType: "merger-prompt", 6 | icon: "AiOutlineMergeCells", 7 | inputNames: ["input-1", "input-2"], 8 | fields: [ 9 | { 10 | name: "mergeMode", 11 | label: "", 12 | type: "option", 13 | options: [ 14 | { 15 | label: "Merge", 16 | value: "1", 17 | }, 18 | { 19 | label: "Merge + GPT", 20 | value: "2", 21 | default: true, 22 | }, 23 | ], 24 | hidden: true, 25 | }, 26 | { 27 | name: "inputNameBar", 28 | type: "inputNameBar", 29 | associatedField: "prompt", 30 | }, 31 | { 32 | name: "prompt", 33 | type: "textarea", 34 | required: true, 35 | placeholder: "MergePromptPlaceholder", 36 | }, 37 | ], 38 | outputType: "markdown", 39 | hasInputHandle: true, 40 | section: "tools", 41 | helpMessage: "mergerPromptHelp", 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/nodeConfig.ts: -------------------------------------------------------------------------------- 1 | import dallENodeConfig from "./dallENode"; 2 | import inputTextNodeConfig from "./inputTextNode"; 3 | import { llmPromptNodeConfig } from "./llmPrompt"; 4 | import stableDiffusionStabilityAiNodeConfig from "./stableDiffusionStabilityAiNode"; 5 | import { urlNodeConfig } from "./urlNode"; 6 | import { youtubeTranscriptNodeConfig } from "./youtubeTranscriptNode"; 7 | import { mergerPromptNode } from "./mergerPromptNode"; 8 | import { gptVisionNodeConfig } from "./gptVisionNode"; 9 | import { FieldType, NodeConfig } from "./types"; 10 | import { getNodeExtensions } from "../api/nodes"; 11 | import withCache from "../api/cache/withCache"; 12 | 13 | export const nodeConfigs: { [key: string]: NodeConfig | undefined } = { 14 | "input-text": inputTextNodeConfig, 15 | url_input: urlNodeConfig, 16 | "llm-prompt": llmPromptNodeConfig, 17 | "gpt-vision": gptVisionNodeConfig, 18 | youtube_transcript_input: youtubeTranscriptNodeConfig, 19 | "dalle-prompt": dallENodeConfig, 20 | "stable-diffusion-stabilityai-prompt": stableDiffusionStabilityAiNodeConfig, 21 | "merger-prompt": mergerPromptNode, 22 | // add other configs here... 23 | }; 24 | 25 | const fieldTypeWithoutHandle: FieldType[] = [ 26 | "select", 27 | "option", 28 | "boolean", 29 | "slider", 30 | ]; 31 | 32 | export const getConfigViaType = (type: string): NodeConfig | undefined => { 33 | return structuredClone(nodeConfigs[type]); 34 | }; 35 | 36 | export const fieldHasHandle = (fieldType: FieldType): boolean => { 37 | return !fieldTypeWithoutHandle.includes(fieldType); 38 | }; 39 | 40 | export const loadExtensions = async () => { 41 | const extensions = await withCache(getNodeExtensions); 42 | extensions.forEach((extension: NodeConfig) => { 43 | const key = extension.processorType; 44 | if (!key) return; 45 | if (key in nodeConfigs) return; 46 | 47 | nodeConfigs[key] = extension; 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/stableDiffusionStabilityAiNode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | const stableDiffusionStabilityAiNodeConfig: NodeConfig = { 4 | nodeName: "Stable Diffusion", 5 | processorType: "stable-diffusion-stabilityai-prompt", 6 | icon: "StabilityAILogo", 7 | hasInputHandle: true, 8 | inputNames: ["prompt"], 9 | fields: [ 10 | { 11 | type: "textarea", 12 | name: "prompt", 13 | placeholder: "DallEPromptPlaceholder", 14 | hideIfParent: true, 15 | }, 16 | { 17 | type: "select", 18 | name: "size", 19 | placeholder: "StableDiffusionSizePlaceholder", 20 | options: [ 21 | { 22 | label: "1024x1024", 23 | value: "1024x1024", 24 | default: true, 25 | }, 26 | { 27 | label: "1152x896", 28 | value: "1152x896", 29 | }, 30 | { 31 | label: "1216x832", 32 | value: "1216x832", 33 | }, 34 | { 35 | label: "1344x768", 36 | value: "1344x768", 37 | }, 38 | { 39 | label: "1536x640", 40 | value: "1536x640", 41 | }, 42 | { 43 | label: "640x1536", 44 | value: "640x1536", 45 | }, 46 | { 47 | label: "768x1344", 48 | value: "768x1344", 49 | }, 50 | { 51 | label: "832x1216", 52 | value: "832x1216", 53 | }, 54 | { 55 | label: "896x1152", 56 | value: "896x1152", 57 | }, 58 | ], 59 | }, 60 | ], 61 | outputType: "imageUrl", 62 | section: "models", 63 | helpMessage: "stableDiffusionPromptHelp", 64 | }; 65 | 66 | export default stableDiffusionStabilityAiNodeConfig; 67 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/urlNode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | export const urlNodeConfig: NodeConfig = { 4 | nodeName: "EnterURL", 5 | processorType: "url_input", 6 | icon: "FaLink", 7 | showHandlesNames: true, 8 | fields: [ 9 | { 10 | name: "url", 11 | type: "input", 12 | required: true, 13 | label: "url", 14 | hasHandle: true, 15 | placeholder: "URLPlaceholder", 16 | }, 17 | { 18 | name: "with_html_tags", 19 | type: "boolean", 20 | label: "with_html_tags", 21 | }, 22 | { 23 | name: "with_html_attributes", 24 | type: "boolean", 25 | label: "with_html_attributes", 26 | }, 27 | { 28 | name: "selectors", 29 | type: "list", 30 | label: "selectors", 31 | placeholder: "div .article #id", 32 | }, 33 | { 34 | name: "selectors_to_remove", 35 | type: "list", 36 | label: "selectors_to_remove", 37 | placeholder: "div .article #id", 38 | defaultValue: ["meta", "link", "script"], 39 | }, 40 | ], 41 | outputType: "text", 42 | defaultHideOutput: true, 43 | section: "input", 44 | helpMessage: "urlInputHelp", 45 | }; 46 | -------------------------------------------------------------------------------- /packages/ui/src/nodes-configuration/youtubeTranscriptNode.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from "./types"; 2 | 3 | export const youtubeTranscriptNodeConfig: NodeConfig = { 4 | nodeName: "YoutubeTranscriptNodeName", 5 | processorType: "youtube_transcript_input", 6 | icon: "YoutubeLogo", 7 | fields: [ 8 | { 9 | name: "url", 10 | label: "url", 11 | type: "input", 12 | required: true, 13 | placeholder: "URLPlaceholder", 14 | hasHandle: true, 15 | }, 16 | { 17 | name: "language", 18 | label: "language", 19 | type: "select", 20 | options: [ 21 | { 22 | label: "English", 23 | value: "en", 24 | default: true, 25 | }, 26 | { 27 | label: "French", 28 | value: "fr", 29 | }, 30 | { 31 | label: "Spanish", 32 | value: "es", 33 | }, 34 | { 35 | label: "German", 36 | value: "de", 37 | }, 38 | { 39 | label: "Italian", 40 | value: "it", 41 | }, 42 | { 43 | label: "Chinese", 44 | value: "zh", 45 | }, 46 | { 47 | label: "Hindi", 48 | value: "hi", 49 | }, 50 | { 51 | label: "Arabic", 52 | value: "ar", 53 | }, 54 | { 55 | label: "Japanese", 56 | value: "ja", 57 | }, 58 | { 59 | label: "Portuguese", 60 | value: "pt", 61 | }, 62 | 63 | { 64 | label: "Russian", 65 | value: "ru", 66 | }, 67 | 68 | { 69 | label: "Korean", 70 | value: "ko", 71 | }, 72 | ], 73 | }, 74 | ], 75 | outputType: "text", 76 | defaultHideOutput: true, 77 | section: "input", 78 | helpMessage: "youtubeTranscriptHelp", 79 | showHandlesNames: true, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/ui/src/providers/FlowDataProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, ReactNode } from "react"; 2 | import { FlowMetadata, FlowTab } from "../layout/main-layout/AppLayout"; 3 | import { Edge, Node } from "reactflow"; 4 | 5 | interface FlowDataContextType { 6 | getCurrentTab: () => FlowTab; 7 | updateCurrentTabMetadata: (metadata: FlowMetadata) => void; 8 | } 9 | 10 | export const FlowDataContext = createContext( 11 | undefined, 12 | ); 13 | 14 | interface FlowDataProviderProps { 15 | children: ReactNode; 16 | flowTab: FlowTab; 17 | onFlowChange: (nodes: Node[], edges: Edge[], metadata?: FlowMetadata) => void; 18 | } 19 | 20 | export const FlowDataProvider: React.FC = ({ 21 | children, 22 | flowTab, 23 | onFlowChange, 24 | }) => { 25 | function getCurrentTab() { 26 | return flowTab; 27 | } 28 | 29 | function updateCurrentTabMetadata(metadata: FlowMetadata) { 30 | onFlowChange(flowTab.nodes, flowTab.edges, metadata); 31 | } 32 | 33 | const value = { 34 | getCurrentTab: getCurrentTab, 35 | updateCurrentTabMetadata: updateCurrentTabMetadata, 36 | }; 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | export const useFlowData = (): FlowDataContextType => { 46 | const context = useContext(FlowDataContext); 47 | if (context === undefined) { 48 | throw new Error("useVisibility must be used within a VisibilityProvider"); 49 | } 50 | return context; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ui/src/providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | // ThemeProvider.tsx 2 | import React, { 3 | createContext, 4 | useState, 5 | ReactNode, 6 | useEffect, 7 | CSSProperties, 8 | } from "react"; 9 | import { ThemeProvider as StyledThemeProvider } from "styled-components"; 10 | import { theme } from "../components/shared/theme"; 11 | import useLocalStorage from "../hooks/useLocalStorage"; 12 | 13 | interface ThemeContextType { 14 | dark: boolean; 15 | toggleTheme: () => void; 16 | getStyle: () => any; 17 | } 18 | 19 | export const ThemeContext = createContext({ 20 | dark: false, 21 | toggleTheme: () => {}, 22 | getStyle: () => ({}), // return an empty style object as a default 23 | }); 24 | interface ThemeProviderProps { 25 | children: ReactNode; 26 | } 27 | 28 | export const ThemeProvider = ({ children }: ThemeProviderProps) => { 29 | const [dark, setDark] = useLocalStorage("darkMode", true); 30 | 31 | useEffect(() => { 32 | // Toggle the "dark" class on for Tailwind 33 | document.documentElement.classList.toggle("dark", dark); 34 | 35 | // Update Mantine's color schema attribute on 36 | document.documentElement.setAttribute( 37 | "data-mantine-color-scheme", 38 | dark ? "dark" : "light", 39 | ); 40 | }, [dark]); 41 | 42 | const toggleTheme = () => { 43 | setDark(!dark); 44 | }; 45 | 46 | const getStyle = () => { 47 | return dark ? theme.dark : theme.light; 48 | }; 49 | 50 | return ( 51 | 52 | 53 | {children} 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /packages/ui/src/services/tabStorage.ts: -------------------------------------------------------------------------------- 1 | import { FlowTab } from "../layout/main-layout/AppLayout"; 2 | 3 | const LOCAL_STORAGE_TAB_KEY = "flowTabs"; 4 | const LOCAL_STORAGE_CURRENT_TAB_KEY = "currentTab"; 5 | 6 | export function getCurrentTabIndex() { 7 | const savedTabIndex = localStorage.getItem(LOCAL_STORAGE_CURRENT_TAB_KEY); 8 | if (!savedTabIndex) return undefined; 9 | return savedTabIndex; 10 | } 11 | 12 | export function saveCurrentTabIndex(index: number) { 13 | localStorage.setItem(LOCAL_STORAGE_CURRENT_TAB_KEY, index.toString()); 14 | } 15 | 16 | export function getLocalTabs() { 17 | const savedTabs = localStorage.getItem(LOCAL_STORAGE_TAB_KEY); 18 | if (!savedTabs) return undefined; 19 | return JSON.parse(savedTabs)?.tabs as FlowTab[]; 20 | } 21 | 22 | export function saveTabsLocally(tabs: FlowTab[]) { 23 | if (!tabs) return; 24 | if (tabs.length >= 1 && tabs[0].nodes.length !== 0) { 25 | const tabsToStore = { tabs: tabs }; 26 | localStorage.setItem(LOCAL_STORAGE_TAB_KEY, JSON.stringify(tabsToStore)); 27 | } 28 | } 29 | 30 | export async function getAllTabs() { 31 | const savedFlowTabs = getLocalTabs(); 32 | return savedFlowTabs ?? []; 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | 7 | // Object.defineProperty(window, "matchMedia", { 8 | // writable: true, 9 | // value: vi.fn().mockImplementation((query) => ({ 10 | // matches: false, 11 | // media: query, 12 | // onchange: null, 13 | // addListener: vi.fn(), // deprecated 14 | // removeListener: vi.fn(), // deprecated 15 | // addEventListener: vi.fn(), 16 | // removeEventListener: vi.fn(), 17 | // dispatchEvent: vi.fn(), 18 | // })), 19 | // }); 20 | 21 | // // Mock ResizeObserver 22 | // global.ResizeObserver = class { 23 | // observe() {} 24 | // unobserve() {} 25 | // disconnect() {} 26 | // }; 27 | -------------------------------------------------------------------------------- /packages/ui/src/sockets/flowEventTypes.ts: -------------------------------------------------------------------------------- 1 | export interface FlowOnProgressEventData { 2 | instanceName: string; 3 | output: T; 4 | isDone: boolean; 5 | } 6 | 7 | export interface FlowOnErrorEventData { 8 | instanceName: string; 9 | nodeName: string; 10 | error: string; 11 | } 12 | 13 | export interface FlowOnCurrentNodeRunningEventData { 14 | instanceName: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/src/sockets/flowSocket.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from "socket.io-client"; 2 | 3 | export type FlowEventIn = 4 | | "connect" 5 | | "progress" 6 | | "error" 7 | | "run_end" 8 | | "current_node_running" 9 | | "reconnect_error" 10 | | "disconnect"; 11 | 12 | export type FlowEventOut = "run_node" | "process_file" | "update_app_config"; 13 | 14 | export class FlowSocket { 15 | private socket: Socket; 16 | 17 | constructor(socket: Socket) { 18 | this.socket = socket; 19 | } 20 | 21 | public on(event: FlowEventIn, handler: (...args: any[]) => void): void { 22 | this.socket.on(event, handler); 23 | } 24 | 25 | public off(event: FlowEventIn, handler: (...args: any[]) => void): void { 26 | this.socket.off(event, handler); 27 | } 28 | 29 | public emit(event: FlowEventOut, ...args: any[]): void { 30 | this.socket.emit(event, ...args); 31 | } 32 | 33 | public connect(): void { 34 | if (!this.socket.connected) { 35 | this.socket.connect(); 36 | } 37 | } 38 | 39 | public disconnect(): void { 40 | if (this.socket.connected) { 41 | this.socket.disconnect(); 42 | } 43 | } 44 | 45 | public close(): void { 46 | this.socket.close(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/ui/src/utils/navigatorUtils.ts: -------------------------------------------------------------------------------- 1 | export const copyToClipboard = (text: string) => { 2 | if (window.isSecureContext) { 3 | navigator.clipboard 4 | .writeText(text) 5 | .then(() => { 6 | console.log("Text copied to clipboard successfully!"); 7 | }) 8 | .catch((err) => { 9 | console.error("Failed to copy text: ", err); 10 | }); 11 | } else { 12 | const textarea = document.createElement("textarea"); 13 | textarea.value = text; 14 | 15 | textarea.style.position = "absolute"; 16 | textarea.style.left = "-99999999px"; 17 | 18 | document.body.prepend(textarea); 19 | 20 | textarea.select(); 21 | 22 | try { 23 | document.execCommand("copy"); 24 | } catch (err) { 25 | console.log(err); 26 | } finally { 27 | textarea.remove(); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ui/src/utils/nodeUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export const generatedIdIdentifier = "#"; 3 | 4 | export const createUniqNodeId = (suffix: string) => { 5 | return ( 6 | Math.random().toString(36).substr(2, 9) + generatedIdIdentifier + suffix 7 | ); 8 | }; -------------------------------------------------------------------------------- /packages/ui/src/utils/toastUtils.tsx: -------------------------------------------------------------------------------- 1 | import { IconType } from "react-icons/lib"; 2 | import { toast } from "react-toastify"; 3 | 4 | export function toastInfoMessage(message: string, id?: string) { 5 | toast.info(message, { 6 | toastId: id, 7 | position: "top-center", 8 | autoClose: 5000, 9 | hideProgressBar: false, 10 | closeOnClick: true, 11 | pauseOnHover: true, 12 | draggable: true, 13 | progress: undefined, 14 | theme: "dark", 15 | }); 16 | } 17 | 18 | export function toastErrorMessage(message: string) { 19 | toast.error( 20 |
{message}
, 21 | { 22 | position: "top-center", 23 | autoClose: 5000, 24 | hideProgressBar: false, 25 | closeOnClick: true, 26 | pauseOnHover: true, 27 | draggable: true, 28 | progress: undefined, 29 | theme: "dark", 30 | }, 31 | ); 32 | } 33 | 34 | export function toastFastSuccessMessage(message: string) { 35 | toast.success(message, { 36 | position: "top-center", 37 | autoClose: 500, 38 | hideProgressBar: true, 39 | closeOnClick: true, 40 | pauseOnHover: false, 41 | draggable: false, 42 | progress: undefined, 43 | theme: "dark", 44 | }); 45 | } 46 | 47 | export function toastFastInfoMessage(message: string) { 48 | toast.info(message, { 49 | position: "top-center", 50 | autoClose: 500, 51 | hideProgressBar: true, 52 | closeOnClick: true, 53 | pauseOnHover: false, 54 | draggable: false, 55 | progress: undefined, 56 | theme: "dark", 57 | }); 58 | } 59 | 60 | export function toastCustomIconInfoMessage(message: string, icon: IconType) { 61 | toast.info(message, { 62 | position: "top-center", 63 | autoClose: 5000, 64 | hideProgressBar: false, 65 | closeOnClick: true, 66 | pauseOnHover: true, 67 | draggable: true, 68 | progress: undefined, 69 | theme: "dark", 70 | icon: icon, 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /packages/ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "jit", 4 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 5 | theme: { 6 | extend: { 7 | colors: { 8 | "custom-dark": "#1E1E1F", 9 | "custom-darker": "#1B1B1C", 10 | "custom-lighter": "#232324", 11 | }, 12 | backgroundImage: { 13 | "subtle-gradient": 14 | "linear-gradient(to bottom, #232324, #1E1E1F, #1B1B1C)", 15 | "app-dark-gradient": 16 | "linear-gradient(0deg, hsl(180deg 9.33% 12.39%) 0%, hsl(0deg 0% 10.39%) 100%)", 17 | "dark-zinc-to-sky": 18 | "linear-gradient(16deg, rgba(9,9,11,1) 0%, rgba(0,212,255,0.05) 100%)", 19 | "ai-flow-g-simple": "url('/backgrounds/g-simple.png')", 20 | "ai-flow-g-particles": "url('/backgrounds/g-particles.png')", 21 | }, 22 | }, 23 | }, 24 | plugins: [], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui/test/e2e/intro-flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { baseURL, waitForAppInitialRender } from "../utils"; 3 | 4 | test("initial gpt node is present after loading", async ({ page }) => { 5 | await page.goto(baseURL); 6 | await waitForAppInitialRender(page); 7 | 8 | const mainContent = await page.$("#main-content"); 9 | expect(mainContent).not.toBeNull(); 10 | 11 | const reactFlow = await page.waitForSelector(".reactflow-wrapper", { 12 | state: "visible", 13 | }); 14 | expect(reactFlow).not.toBeNull(); 15 | 16 | const gptNode = await page.waitForSelector(".react-flow__node-llm-prompt", { 17 | state: "visible", 18 | }); 19 | 20 | expect(gptNode).not.toBeNull(); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/ui/test/e2e/loading-screen.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { baseURL } from "../utils"; 3 | 4 | test("renders the loading screen initially", async ({ page }) => { 5 | await page.goto(baseURL); 6 | 7 | await page.waitForSelector("#loading-screen", { state: "visible" }); 8 | const loadingScreen = await page.$("#loading-screen"); 9 | expect(loadingScreen).not.toBeNull(); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/ui/test/e2e/main-content.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { baseURL, waitForAppInitialRender } from "../utils"; 3 | 4 | test("renders the main content after loading", async ({ page }) => { 5 | await page.goto(baseURL); 6 | await waitForAppInitialRender(page); 7 | 8 | const mainContent = await page.$("#main-content"); 9 | expect(mainContent).not.toBeNull(); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/ui/test/e2e/sidebar-default-nodes.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { baseURL, waitForAppInitialRender } from "../utils"; 3 | 4 | test("default nodes are loaded in sidebar", async ({ page }) => { 5 | await page.goto(baseURL); 6 | await waitForAppInitialRender(page); 7 | 8 | const textNodeSidebar = await page.locator("text=Text").first(); 9 | const webNodeSidebar = await page.locator("text=Web Extractor").first(); 10 | const fileNodeSidebar = await page.locator("text=File").first(); 11 | 12 | const textNodeContent = await textNodeSidebar.textContent(); 13 | const webNodeContent = await webNodeSidebar.textContent(); 14 | const fileNodeContent = await fileNodeSidebar.textContent(); 15 | 16 | expect(textNodeContent).toContain("Text"); 17 | expect(webNodeContent).toContain("Web Extractor"); 18 | expect(fileNodeContent).toContain("File"); 19 | }); 20 | 21 | test("hidden nodes are not loaded in sidebar", async ({ page }) => { 22 | await page.goto(baseURL); 23 | await waitForAppInitialRender(page); 24 | 25 | const aiActionNodeSidebar = await page.locator("text=AI Action"); 26 | 27 | const aiActionNodeCount = await aiActionNodeSidebar.count(); 28 | 29 | expect(aiActionNodeCount).toBe(0); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/ui/test/e2e/sidebar-extensions-nodes.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { baseURL, waitForAppInitialRender } from "../utils"; 3 | 4 | test("default extensions are loaded in sidebar", async ({ page }) => { 5 | await page.goto(baseURL); 6 | await waitForAppInitialRender(page); 7 | 8 | const stabilityNodeSidebar = await page.locator("text=StabilityAI").first(); 9 | const documentNodeSidebar = await page 10 | .locator("text=Document-to-Text") 11 | .first(); 12 | 13 | const stabilityaiNodeContent = await stabilityNodeSidebar.textContent(); 14 | const documentNodeContent = await documentNodeSidebar.textContent(); 15 | 16 | expect(stabilityaiNodeContent).toContain("StabilityAI"); 17 | expect(documentNodeContent).toContain("Document-to-Text"); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/ui/test/e2e/tuto-display.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { baseURL, waitForAppInitialRender } from "../utils"; 3 | 4 | test("tuto is launched after loading", async ({ page }) => { 5 | await page.goto(baseURL); 6 | await waitForAppInitialRender(page); 7 | 8 | await page.screenshot({ path: "screenshots/tuto-before-wait.png" }); 9 | await page.waitForSelector("#react-joyride-step-0", { state: "attached" }); 10 | 11 | const tutoStep = await page.$("#react-joyride-step-0"); 12 | expect(tutoStep).not.toBeNull(); 13 | 14 | const textContent = await tutoStep?.textContent(); 15 | expect(textContent).toContain("Welcome to AI-FLOW"); 16 | 17 | const closeTutoButton = await page.locator("text=I know the app"); 18 | await closeTutoButton.click(); 19 | 20 | const tutoStepAfterClose = await page.$("#react-joyride-step-0"); 21 | expect(tutoStepAfterClose).toBeNull(); 22 | 23 | await page.screenshot({ path: "screenshots/tuto-after-wait.png" }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/ui/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | const dotenv = require("dotenv"); 3 | 4 | dotenv.config({ path: ".env.test" }); 5 | 6 | export const baseURL = process.env.E2E_TEST_BASE_URL || "http://localhost:80"; 7 | 8 | export async function waitForAppInitialRender(page: Page) { 9 | await page.waitForSelector("#main-content", { state: "visible" }); 10 | await page.waitForSelector("#loading-screen", { state: "detached" }); 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals"] 23 | }, 24 | "include": [ 25 | "src", 26 | "test" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: "/", 7 | plugins: [react()], 8 | preview: { 9 | port: 3000, 10 | }, 11 | server: { 12 | port: 3000, 13 | }, 14 | build: { 15 | outDir: "./build", 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/ui/vitest.config.ts: -------------------------------------------------------------------------------- 1 | // vitest.config.ts 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | setupFiles: "src/setupTests.ts", 8 | include: ["test/unit/**/*.test.{js,ts,jsx,tsx}"], 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------