diff --git a/.env.local b/.env.local index f8c71bbc..01beed63 100644 --- a/.env.local +++ b/.env.local @@ -2,9 +2,10 @@ EDITION=ce NEXT_PUBLIC_EDITION=ce SAGITTARIUS_GRAPHQL_URL=http://localhost:80/graphql +SAGITTARIUS_CABLE_URL=http://localhost:80/cable NEXT_PUBLIC_SCULPTOR_VERSION=0.0.0 -NEXT_PUBLIC_PICTOR_VERSION=0.10.3 +NEXT_PUBLIC_PICTOR_VERSION=0.10.4 NEXT_PUBLIC_ALLOWED_REDIRECT_DOMAINS=*.code0.tech,*.codezero.build NEXT_PUBLIC_OTEL_SERVICE_NAME=#"sculptor-client" diff --git a/next.config.ts b/next.config.ts index 39f4b383..8e539e00 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import path from "node:path"; const EDITION = process.env.EDITION ?? "ce"; const SAGITTARIUS_GRAPHQL_URL = process.env.SAGITTARIUS_GRAPHQL_URL ?? 'http://localhost:3010/graphql'; +const SAGITTARIUS_CABLE_URL = process.env.SAGITTARIUS_CABLE_URL ?? 'http://localhost:3010/cable'; const cspHeader = ` default-src 'self'; @@ -15,7 +16,7 @@ const cspHeader = ` form-action 'self'; frame-ancestors 'none'; worker-src 'self' blob: data: *; - connect-src 'self' ${SAGITTARIUS_GRAPHQL_URL} ${process.env.NEXT_PUBLIC_OTEL_LOGS_ENDPOINT} ${process.env.NEXT_PUBLIC_OTEL_TRACES_ENDPOINT}; + connect-src 'self' ${SAGITTARIUS_GRAPHQL_URL} ${SAGITTARIUS_CABLE_URL.replace("http", "ws")} ${process.env.NEXT_PUBLIC_OTEL_LOGS_ENDPOINT} ${process.env.NEXT_PUBLIC_OTEL_TRACES_ENDPOINT}; ` const nextConfig: NextConfig = { @@ -55,6 +56,10 @@ const nextConfig: NextConfig = { { source: '/graphql', destination: SAGITTARIUS_GRAPHQL_URL // Proxy to Backend + }, + { + source: '/cable', + destination: SAGITTARIUS_CABLE_URL // Proxy to Backend } ]; } diff --git a/package-lock.json b/package-lock.json index faeec56f..a19facf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,11 @@ "version": "0.0.0", "dependencies": { "@apollo/client": "^4.0.9", - "@code0-tech/pictor": "^0.10.3", - "@code0-tech/triangulum": "^0.22.0", + "@code0-tech/pictor": "^0.10.4", + "@code0-tech/triangulum": "^0.25.0", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lint": "^6.9.5", + "@icons-pack/react-simple-icons": "^13.13.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/context-zone": "^2.6.1", "@opentelemetry/exporter-logs-otlp-http": "^0.218.0", @@ -21,11 +22,14 @@ "@opentelemetry/instrumentation-fetch": "^0.218.0", "@opentelemetry/instrumentation-xml-http-request": "^0.218.0", "@opentelemetry/sdk-trace-web": "^2.6.1", + "@rails/actioncable": "^8.1.300", + "@types/rails__actioncable": "^8.0.3", "@uidotdev/usehooks": "^2.4.1", "@vercel/otel": "^2.1.1", "@xyflow/react": "^12.11.0", "date-fns": "^4.1.0", "graphql": "^16.12.0", + "graphql-ruby-client": "^1.15.1", "graphql-tag": "^2.12.6", "ldrs": "^1.1.9", "lodash": "^4.18.1", @@ -54,6 +58,7 @@ "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.9.tgz", "integrity": "sha512-qfpkQD51tdU/7iAR6aLb4w9o/L7I475DluWHRb61U/3Q0AH29nNOxOBHjBbWDdf16ncPOoQuxne1sEs2NjqBFw==", "license": "MIT", + "peer": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -417,9 +422,9 @@ } }, "node_modules/@code0-tech/pictor": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@code0-tech/pictor/-/pictor-0.10.3.tgz", - "integrity": "sha512-p6hW6mtbLJ7+KgvyzQ3rVPg5svU8RiVLcf6kt/4+PknQ1A1DQ/k7KJ/6mdTSD0/c2dHrY3dKdoXVcpnl2muOSw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@code0-tech/pictor/-/pictor-0.10.4.tgz", + "integrity": "sha512-cbFJJ+ijM9d9OOxhxNuiVVghKs5wgc79QCvxIoH0pOIJ/ASCAfOMFh3pIh5gJCQWURddcwkfWhC7Y+wflnozDg==", "peerDependencies": { "@ark-ui/react": "^5.36.2", "@codemirror/autocomplete": "^6.20.0", @@ -444,7 +449,6 @@ "@tabler/icons-react": "3.41.1", "@uiw/codemirror-themes": "^4.25.4", "@uiw/react-codemirror": "^4.25.4", - "@xyflow/react": "^12.10.0", "avvvatars-react": "^0.4.2", "cmdk": "^1.1.1", "js-md5": "^0.8.3", @@ -459,17 +463,17 @@ } }, "node_modules/@code0-tech/sagittarius-graphql-types": { - "version": "0.0.0-experimental-2585797094-74c645eca45310e3506df6a95c4fab1a2d6abbc7", - "resolved": "https://registry.npmjs.org/@code0-tech/sagittarius-graphql-types/-/sagittarius-graphql-types-0.0.0-experimental-2585797094-74c645eca45310e3506df6a95c4fab1a2d6abbc7.tgz", - "integrity": "sha512-nWg7Jb1bKd49fTb2vGuKk0KPN9mkM+y2EK4z8VVNk21J2xwIb47NazJuP9V8o4WZgCbg4d486IqHDRa5uJweiQ==", + "version": "0.0.0-experimental-2616186698-14d97e4ac998067751f9cbd9f8fc05c6fed628fe", + "resolved": "https://registry.npmjs.org/@code0-tech/sagittarius-graphql-types/-/sagittarius-graphql-types-0.0.0-experimental-2616186698-14d97e4ac998067751f9cbd9f8fc05c6fed628fe.tgz", + "integrity": "sha512-8TqdH9pFs1HQjxqGtxgFslnKdvlVBaKooKrWvpPK4LGdYwu8n1MZFSqXu6sl4+OvrPaTEQxqhGdScjut+da9DA==", "peer": true }, "node_modules/@code0-tech/triangulum": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@code0-tech/triangulum/-/triangulum-0.22.0.tgz", - "integrity": "sha512-XF1MnG8IA2LvJ0IhA/YBzcH8meXUUpJ5gSwOpABhD8H0Fb4IJPynwm1KmMh977fTAH7j+aD6RpEqdZTU8NUw1w==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@code0-tech/triangulum/-/triangulum-0.25.0.tgz", + "integrity": "sha512-Mm1Ed49EUuSlOYe6jeRdjepG6oSHj+vcKuYIWfGKPLUM1eW884aD6hWqqCXxCerMbinCFuH3q1MOH/CTSiYouw==", "peerDependencies": { - "@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2585797094-74c645eca45310e3506df6a95c4fab1a2d6abbc7", + "@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2616186698-14d97e4ac998067751f9cbd9f8fc05c6fed628fe", "@typescript/vfs": "^1.6.4", "lossless-json": "^4.3.0", "typescript": "^5.9.3 || ^6.0.2" @@ -877,6 +881,19 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@icons-pack/react-simple-icons": { + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-13.13.0.tgz", + "integrity": "sha512-B5HhQMIpcSH4z8IZ8HFhD59CboHceKYMpPC9kAwGyKntvPdyJJv26DLu4Z1wAjcCLyrJhf11tMhiQGom9Rxb9g==", + "license": "MIT", + "engines": { + "node": ">=24", + "pnpm": ">=10" + }, + "peerDependencies": { + "react": "^16.13 || ^17 || ^18 || ^19" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -3342,6 +3359,12 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@rails/actioncable": { + "version": "8.1.300", + "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.1.300.tgz", + "integrity": "sha512-zOENQsq3NM2jyBY6Z2qtZa3V/R/6OEqA+LGKixQbBMl7kk/J3FXDRcszPe74LsHNgB01jCl/DXu/xA8sHt4I/g==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3490,6 +3513,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/rails__actioncable": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/rails__actioncable/-/rails__actioncable-8.0.3.tgz", + "integrity": "sha512-y46MOTYorVQVwlHUyaZYbrh3nIkXsRYNuPna32lb3RngLVBlndNbIPvAUywFfhivftNhYg+vW5sZKWYCVIX2lA==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -4222,7 +4251,6 @@ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz", "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==", "license": "MIT", - "peer": true, "dependencies": { "@xyflow/system": "0.0.77", "classcat": "^5.0.3", @@ -7048,6 +7076,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7061,6 +7106,42 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", @@ -7123,6 +7204,26 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-ruby-client": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/graphql-ruby-client/-/graphql-ruby-client-1.15.1.tgz", + "integrity": "sha512-dSn9+zL+4Q7PxLMl8uHpj4wmZbYYer4czer3HKdU3fH25ZUSxs+t06DmjYV/1bFN7UwwLpzot2b8IrW0IyEw7g==", + "license": "LGPL-3.0", + "dependencies": { + "glob": "^13.0.6", + "minimist": "^1.2.0" + }, + "bin": { + "graphql-ruby-client": "cli.js" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@apollo/client": ">=3.3.6", + "graphql": ">=14.3.1" + } + }, "node_modules/graphql-tag": { "version": "2.12.6", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", @@ -7979,12 +8080,20 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -8377,6 +8486,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/perfect-freehand": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.3.tgz", diff --git a/package.json b/package.json index c81b86a4..2884ce6e 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,11 @@ }, "dependencies": { "@apollo/client": "^4.0.9", - "@code0-tech/pictor": "^0.10.3", - "@code0-tech/triangulum": "^0.22.0", + "@code0-tech/pictor": "^0.10.4", + "@code0-tech/triangulum": "^0.25.0", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lint": "^6.9.5", + "@icons-pack/react-simple-icons": "^13.13.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/context-zone": "^2.6.1", "@opentelemetry/exporter-logs-otlp-http": "^0.218.0", @@ -27,11 +28,14 @@ "@opentelemetry/instrumentation-fetch": "^0.218.0", "@opentelemetry/instrumentation-xml-http-request": "^0.218.0", "@opentelemetry/sdk-trace-web": "^2.6.1", + "@rails/actioncable": "^8.1.300", + "@types/rails__actioncable": "^8.0.3", "@uidotdev/usehooks": "^2.4.1", "@vercel/otel": "^2.1.1", "@xyflow/react": "^12.11.0", "date-fns": "^4.1.0", "graphql": "^16.12.0", + "graphql-ruby-client": "^1.15.1", "graphql-tag": "^2.12.6", "ldrs": "^1.1.9", "lodash": "^4.18.1", diff --git a/src/app/(flow)/layout.tsx b/src/app/(flow)/layout.tsx index 62352cbf..2e76f787 100644 --- a/src/app/(flow)/layout.tsx +++ b/src/app/(flow)/layout.tsx @@ -33,6 +33,7 @@ import {RoleView} from "@edition/role/services/Role.view"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; import {ModuleService} from "@edition/module/services/Module.service"; +import {AIService, Model} from "@edition/ai/services/AI.service"; export default function FlowLayout({bar, tab, children}: { bar: React.ReactNode, @@ -67,7 +68,7 @@ export default function FlowLayout({bar, tab, children}: { const datatype = usePersistentReactiveArrayService(`dashboard::datatypes::${currentSession?.id}`, (store) => new DatatypeService(graphqlClient, store)) const flowtype = usePersistentReactiveArrayService(`dashboard::flowtypes::${currentSession?.id}`, (store) => new FlowTypeService(graphqlClient, store)) const module = usePersistentReactiveArrayService(`dashboard::modules::${currentSession?.id}`, (store) => new ModuleService(graphqlClient, store)) - + const ai = usePersistentReactiveArrayService(`dashboard::ai::${currentSession?.id}`, (store) => new AIService(graphqlClient, store)) const runtimeId = React.useMemo(() => project[1].getById(projectId, {namespaceId})?.primaryRuntime?.id, [projectId, project[0], namespaceId]) @@ -82,7 +83,7 @@ export default function FlowLayout({bar, tab, children}: { }, [runtimeId, namespaceId, projectId, currentSession, flow, functions, datatype, flowtype]) return + services={[user, organization, member, namespace, runtime, project, role, flow, functions, datatype, flowtype, module, ai]}>
(undefined); + const reactFlow = useReactFlow() + + useHotkeys('shift+1', (keyboardEvent) => { + setTab(prevState => prevState === "file" ? undefined : "file") + keyboardEvent.stopPropagation() + keyboardEvent.preventDefault() + }, []) + + useHotkeys('shift+2', (keyboardEvent) => { + setTab(prevState => prevState === "execution" ? undefined : "execution") + keyboardEvent.stopPropagation() + keyboardEvent.preventDefault() + }, []) + + React.useEffect(() => { + + const localSelectedNode = reactFlow.getNodes().filter((node) => node.selected)[0] as Node | undefined + if (!localSelectedNode) return + setTimeout(() => { + reactFlow.fitView({ + nodes: [{id: localSelectedNode.id}], + maxZoom: reactFlow.getZoom(), + minZoom: 1, + }); + }, 100) + }, [tab, reactFlow]) return - + style={{ + ...(tab === "execution" ? {borderRadius: "1rem"} : { + borderTopLeftRadius: "1rem", + borderTopRightRadius: "1rem" + }) + }}> + { tab === "execution" && ( @@ -63,7 +100,7 @@ export default function Page() { {tab === "file" && ( <> - diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx index 8be44185..b1955ec6 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx @@ -1,5 +1,6 @@ "use client" import {FlowLayout} from "@edition/flow/layout/FlowLayout"; +import '@xyflow/react/dist/style.css'; export default FlowLayout diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx index a89644ad..45049222 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx @@ -1,68 +1,5 @@ "use client" -import { - Button, - Col, - Flex, - ScrollArea, - ScrollAreaScrollbar, - ScrollAreaThumb, - ScrollAreaViewport, - Spacing, - Text -} from "@code0-tech/pictor"; -import React from "react"; -import Link from "next/link"; -import {useParams} from "next/navigation"; -import {ResizablePanel} from "@code0-tech/pictor/dist/components/resizable/Resizable"; +import {FlowOverviewPage} from "@edition/flow/pages/FlowOverviewPage"; -export default function Page() { - - const params = useParams() - - const namespaceIndex = params?.namespaceId as any as number - - return - - - - - Create or select a flow - - - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut - labore et dolore magna aliquyam erat, sed diam voluptua. - - - - - - - - - Or start from a template - - - - - - - -
- -} \ No newline at end of file +export default FlowOverviewPage \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5b91cbb2..7edbaa40 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,8 @@ import { HttpLink, InMemoryCache, Observable, - ServerError + ServerError, + split, } from "@apollo/client"; import {ApolloProvider} from "@apollo/client/react"; import React, {Suspense} from "react"; @@ -20,6 +21,8 @@ import {Error} from "@code0-tech/sagittarius-graphql-types"; import {Inter} from 'next/font/google' import {GraphQLFormattedError} from "graphql/error"; import {addIslandErrorNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {createConsumer} from "@rails/actioncable"; +import ActionCableLink from "graphql-ruby-client/subscriptions/ActionCableLink"; /** * Load the Inter font with Latin subset and swap display strategy @@ -170,21 +173,48 @@ export default function RootLayout({children}: Readonly<{ children: React.ReactN /** * Apollo Client instance with configured links and cache */ - const client = React.useMemo(() => new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.from([errorLink, authMiddleware, responseHandlerLink, new HttpLink({uri: "/graphql"})]), - defaultOptions: { - watchQuery: { - errorPolicy: "all", - }, - query: { - errorPolicy: "all", + const client = React.useMemo(() => { + const cable = createConsumer("/cable") + + const getToken = () => { + try { + const raw = localStorage.getItem("ide_code-zero_session") + return raw ? JSON.parse(raw)?.token : undefined + } catch { + return undefined + } + } + + const hasSubscriptionOperation = ({query: {definitions}}: any) => { + return definitions.some( + ({kind, operation}: any) => kind === "OperationDefinition" && operation === "subscription" + ) + } + + const actionCableLink = new ActionCableLink({ + cable, + connectionParams: () => { + const token = getToken() + return token ? {token: `Session ${token}`} : {} }, - mutate: { - errorPolicy: "all", + }) + + const link = ApolloLink.split( + hasSubscriptionOperation, + actionCableLink, + ApolloLink.from([errorLink, authMiddleware, responseHandlerLink, new HttpLink({uri: "/graphql"})]), + ) + + return new ApolloClient({ + cache: new InMemoryCache(), + link, + defaultOptions: { + watchQuery: {errorPolicy: "all"}, + query: {errorPolicy: "all"}, + mutate: {errorPolicy: "all"}, }, - }, - }), [authMiddleware]) + }) + }, [authMiddleware]) return React.useMemo(() => { return diff --git a/src/packages/ce/src/ai/components/AIChatComponent.tsx b/src/packages/ce/src/ai/components/AIChatComponent.tsx new file mode 100644 index 00000000..2bf01c8c --- /dev/null +++ b/src/packages/ce/src/ai/components/AIChatComponent.tsx @@ -0,0 +1,344 @@ +import React from "react"; +import { + Button, + ButtonGroup, + Card, + EditorInput, + Flex, + Progress, + SelectContent, + SelectItem, + SelectItemText, + SelectPortal, + SelectTrigger, + SelectValue, + SelectViewport, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import Link from "next/link"; +import {StreamLanguage} from "@codemirror/language"; +import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; +import {Select} from "@radix-ui/react-select"; +import {IconChevronDown, IconPlayerStop, IconSend, IconSparkles2Filled} from "@tabler/icons-react"; +import {AIService} from "@edition/ai/services/AI.service"; +import {motion} from "framer-motion"; +import {Flow, NamespaceProject, Subscription} from "@code0-tech/sagittarius-graphql-types"; +import {useSubscription} from "@apollo/client/react"; +import generateFlowSubscription from "@edition/ai/services/subscriptions/AI.generateFlow.subscription.graphql" + +const GENERATING_VARIANTS = [ + "Generating...", + "Thinking...", + "Analyzing your prompt...", + "Composing flow...", + "Crafting nodes...", + "Wiring it up...", + "Almost there..." +] + +export interface AIChatComponentProps { + projectId: NamespaceProject['id'] + flowId?: Flow['id'] + prompt?: string + onData?: (data: any) => string | void + +} + +export const AIChatComponent: React.FC = (props) => { + + const {projectId, flowId, prompt = "", onData} = props + + const aiService = useService(AIService) + const aiStore = useStore(AIService) + + const [promptState, setPromptState] = React.useState(prompt) + const [model, setModel] = React.useState(undefined) + const [executionIdentifier, setExecutionIdentifier] = React.useState(null) + const [isAIActive, setIsAIActive] = React.useState(false) + const [aiVariantIndex, setAiVariantIndex] = React.useState(0) + const [aiErrorMessage, setAiErrorMessage] = React.useState(null) + + const models = React.useMemo( + () => aiService.values(), + [aiStore] + ) + + const {data} = useSubscription(generateFlowSubscription, { + variables: {executionIdentifier: executionIdentifier}, + skip: !executionIdentifier, + onData: (data) => { + setIsAIActive(true) + if (data.data.data?.aiGenerateFlow?.flow) { + const result = onData?.(data.data.data?.aiGenerateFlow) + if (typeof result === "string") { + setAiErrorMessage(result) + } else { + setPromptState("") + } + setExecutionIdentifier(null) + } else if (data.data.data?.aiGenerateFlow?.flow === null) { + setExecutionIdentifier(null) + setAiErrorMessage("Generation failed. Try another model.") + } + }, + onComplete: () => setIsAIActive(false), + onError: () => { + setIsAIActive(false) + setAiErrorMessage("Generation failed. Try another model.") + setExecutionIdentifier(null) + }, + }) + + const aiLoading = React.useMemo( + () => ((!data || Object.keys(data).length === 0) && executionIdentifier && isAIActive), + [executionIdentifier, data, isAIActive] + ) + + const onSend = React.useCallback(() => { + aiService.generateFlow({ + prompt: promptState, + projectId: projectId!, + modelIdentifier: model!, + flowId: flowId, + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + setExecutionIdentifier(payload?.executionIdentifier ?? null) + } + }) + }, [aiService, model, promptState]) + + React.useEffect(() => { + const id = setInterval( + () => setAiVariantIndex(i => (i + 1) % GENERATING_VARIANTS.length), + 4000 + ) + return () => clearInterval(id) + }, []) + + React.useEffect(() => { + setModel(models.length > 0 + ? models.reduce((max, obj) => (obj?.tokenCost ?? 1) > (max?.tokenCost ?? 1) ? obj : max)?.identifier ?? undefined + : undefined) + }, [models]) + + React.useEffect( + () => setPromptState(prompt), + [prompt, setPromptState] + ) + + return + + + + { + models.length <= 0 && ( + + + + + + + + + + + ) + } + { + aiLoading ? ( +
+ + + {GENERATING_VARIANTS[aiVariantIndex]} + + +
+ ) : ( + { + setPromptState(value) + }} + wrapperComponent={{ + style: { + background: "transparent", + boxShadow: "none" + } + }} + placeholder={"Ask AI anything..."} + language={StreamLanguage.define({ + token(stream) { + stream.next() + return null; + } + })}/> + ) + } + + + + + + + + + {!aiLoading && aiErrorMessage ? ( + + {aiErrorMessage} + + ) : null} + {aiLoading ? ( + + ) : ( + + )} + + + +
+ + + + { + models.length > 0 ? ( + + + Upgrade your license to increase your AI usage limit + + + + ) : ( + + + You currently don't have AI features enabled or don't have any models available + + + ) + } + +
+
+} \ No newline at end of file diff --git a/src/packages/ce/src/ai/services/AI.service.ts b/src/packages/ce/src/ai/services/AI.service.ts new file mode 100644 index 00000000..aadc7b0b --- /dev/null +++ b/src/packages/ce/src/ai/services/AI.service.ts @@ -0,0 +1,56 @@ +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; +import { + AiGenerateFlowInput, AiGenerateFlowPayload, + AiModel, + Mutation, + Query +} from "@code0-tech/sagittarius-graphql-types"; +import {GraphqlClient} from "@core/util/graphql-client"; +import velorumGenerateFlowMutation from "./mutations/AI.generateFlow.mutation.graphql"; +import {Payload, View} from "@code0-tech/pictor/dist/utils/view"; +import velorumModelsQuery from "./queries/AI.models.query.graphql"; + +export type Model = AiModel & Payload + +export class AIService extends ReactiveArrayService { + + private readonly client: GraphqlClient + private i = 0 + + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + super(store) + this.client = client + } + + values(): Model[] { + const models = super.values() + if (models.length > 0) return models + + this.client.query({ + query: velorumModelsQuery, + }).then(result => { + const models = result.data?.ai?.models ?? [] + models.forEach(model => { + if (model && !this.hasById(model.identifier)) { + this.set(this.i++, new View(model as Model)) + } + }) + }) + + return super.values() + } + + hasById(identifier: Model["identifier"]): boolean { + return super.values().some(m => m.identifier === identifier) + } + + async generateFlow(payload: AiGenerateFlowInput): Promise { + const result = await this.client.mutate({ + mutation: velorumGenerateFlowMutation, + variables: {...payload} + }) + + return result.data?.aiGenerateFlow ?? undefined + } + +} diff --git a/src/packages/ce/src/ai/services/fragments/AI.flow.fragment.graphql b/src/packages/ce/src/ai/services/fragments/AI.flow.fragment.graphql new file mode 100644 index 00000000..436501bd --- /dev/null +++ b/src/packages/ce/src/ai/services/fragments/AI.flow.fragment.graphql @@ -0,0 +1,74 @@ +fragment AIFlow on AiGenerationFlow { + __typename + name + startingNodeId + settings { + __typename + id + value + cast + flowSettingIdentifier + } + type { + __typename + id + identifier + } + nodes { + __typename + id + nextNodeId + parameters { + __typename + id + cast + value { + __typename + ... on AiGenerationLiteralValue { + __typename + value + } + ... on AiGenerationReferenceValue { + __typename + id + nodeFunctionId + inputIndex + inputTypeIdentifier + parameterIndex + referencePath { + __typename + path + arrayIndex + } + } + ... on AiGenerationSubFlowValue { + __typename + startingNodeId + signature + settings { + __typename + identifier + defaultValue + hidden + optional + } + functionDefinition { + __typename + id + identifier + } + } + } + parameterDefinition { + __typename + id + identifier + } + } + functionDefinition { + __typename + id + identifier + } + } +} \ No newline at end of file diff --git a/src/packages/ce/src/ai/services/fragments/AI.model.fragment.graphql b/src/packages/ce/src/ai/services/fragments/AI.model.fragment.graphql new file mode 100644 index 00000000..f890e93e --- /dev/null +++ b/src/packages/ce/src/ai/services/fragments/AI.model.fragment.graphql @@ -0,0 +1,7 @@ +fragment AIModel on AiModel { + __typename + identifier + name + tokenCost + types +} diff --git a/src/packages/ce/src/ai/services/mutations/AI.generateFlow.mutation.graphql b/src/packages/ce/src/ai/services/mutations/AI.generateFlow.mutation.graphql new file mode 100644 index 00000000..bd1b2695 --- /dev/null +++ b/src/packages/ce/src/ai/services/mutations/AI.generateFlow.mutation.graphql @@ -0,0 +1,18 @@ +mutation generateFlow($projectId: NamespaceProjectID!, $modelIdentifier: String!, $prompt: String!, $flowId: FlowID) { + aiGenerateFlow(input: { + projectId: $projectId + modelIdentifier: $modelIdentifier + prompt: $prompt + flowId: $flowId + }) { + errors { + ...on Error { + errorCode + details { + __typename + } + } + } + executionIdentifier + } +} diff --git a/src/packages/ce/src/ai/services/queries/AI.models.query.graphql b/src/packages/ce/src/ai/services/queries/AI.models.query.graphql new file mode 100644 index 00000000..820ce54c --- /dev/null +++ b/src/packages/ce/src/ai/services/queries/AI.models.query.graphql @@ -0,0 +1,8 @@ +#import '@edition/ai/services/fragments/AI.model.fragment.graphql' +query models { + ai { + models { + ...AIModel + } + } +} diff --git a/src/packages/ce/src/ai/services/subscriptions/AI.generateFlow.subscription.graphql b/src/packages/ce/src/ai/services/subscriptions/AI.generateFlow.subscription.graphql new file mode 100644 index 00000000..a719d196 --- /dev/null +++ b/src/packages/ce/src/ai/services/subscriptions/AI.generateFlow.subscription.graphql @@ -0,0 +1,9 @@ +#import '@edition/ai/services/fragments/AI.flow.fragment.graphql' + +subscription generateFlow($executionIdentifier: String!) { + aiGenerateFlow(executionIdentifier: $executionIdentifier) { + flow { + ...AIFlow + } + } +} diff --git a/src/packages/ce/src/ai/util/AI.flow.mapper.ts b/src/packages/ce/src/ai/util/AI.flow.mapper.ts new file mode 100644 index 00000000..f26b2b33 --- /dev/null +++ b/src/packages/ce/src/ai/util/AI.flow.mapper.ts @@ -0,0 +1,97 @@ +import { + AiGenerationFlow, + AiGenerationLiteralValue, + AiGenerationNodeValue, + AiGenerationReferenceValue, + AiGenerationSubFlowValue, + FlowInput, + FlowSettingInput, + NodeFunctionInput, + NodeParameterInput, + NodeParameterValueInput, +} from "@code0-tech/sagittarius-graphql-types" + +const FALLBACK_FLOW_NAME = "Untitled flow" + +const mapValue = (value?: AiGenerationNodeValue | null): NodeParameterValueInput => { + switch (value?.__typename) { + case "AiGenerationLiteralValue": + return {literalValue: (value as AiGenerationLiteralValue).value ?? null} + case "AiGenerationReferenceValue": { + const v = value as AiGenerationReferenceValue + return { + referenceValue: { + ...(v.nodeFunctionId ? {nodeFunctionId: v.nodeFunctionId} : {}), + ...(v.parameterIndex != null ? {parameterIndex: v.parameterIndex} : {}), + ...(v.inputIndex != null ? {inputIndex: v.inputIndex} : {}), + ...(v.inputTypeIdentifier ? {inputTypeIdentifier: v.inputTypeIdentifier} : {}), + referencePath: v.referencePath?.map(p => ({ + path: p.path, + arrayIndex: p.arrayIndex, + })) ?? [], + }, + } + } + case "AiGenerationSubFlowValue": { + const v = value as AiGenerationSubFlowValue + return { + subFlowValue: { + ...(v.startingNodeId + ? {startingNodeId: v.startingNodeId} + : v.functionDefinition?.identifier + ? {functionIdentifier: v.functionDefinition.identifier} + : {}), + signature: v.signature ?? "", + settings: v.settings?.map(s => ({ + defaultValue: s.defaultValue, + hidden: s.hidden, + identifier: s.identifier!, + optional: s.optional, + })), + }, + } + } + default: + return {literalValue: null} + } +} + +export const ensureUniqueFlowName = (name: string, existingNames: string[]): string => { + if (!existingNames.includes(name)) return name + let i = 2 + while (existingNames.includes(`${name} v${i}`)) i++ + return `${name} v${i}` +} + +export const mapAiGenerationFlowToFlowInput = ( + aiFlow: AiGenerationFlow, + options?: { existingNames?: string[] } +): FlowInput | undefined => { + if (!aiFlow.type?.id) return undefined + + const baseName = aiFlow.name?.trim() ? aiFlow.name.trim() : FALLBACK_FLOW_NAME + const name = ensureUniqueFlowName(baseName, options?.existingNames ?? []) + + const settings: FlowSettingInput[] = (aiFlow.settings ?? []).map(s => ({ + cast: s?.cast ?? undefined, + value: s?.value, + })) + + const nodes: NodeFunctionInput[] = (aiFlow.nodes ?? []).map(node => ({ + id: node?.id!, + nextNodeId: node?.nextNodeId ?? undefined, + functionDefinitionId: node?.functionDefinition?.id!, + parameters: (node?.parameters ?? []).map(p => ({ + cast: p?.cast ?? undefined, + value: mapValue(p?.value), + })), + })) + + return { + name, + type: aiFlow.type.id, + startingNodeId: aiFlow.startingNodeId ?? undefined, + settings, + nodes, + } +} diff --git a/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx b/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx index c2d10091..950b02c5 100644 --- a/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx +++ b/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx @@ -12,9 +12,6 @@ export const ApplicationBreadcrumbView: React.FC = () => { const namespaceIndex = params.namespaceId as string | undefined const projectIndex = params.projectId as string | undefined - - console.log(path, path.split("/"), path.split("/").filter(value => value != "").slice(0, path.split("/").length)) - return { (namespaceIndex || projectIndex) ? ( diff --git a/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx b/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx index 919319ab..92ecb021 100644 --- a/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx +++ b/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx @@ -4,14 +4,16 @@ import {Badge, BadgeType, Text, Tooltip, TooltipContent, TooltipPortal, TooltipT import {truncateText} from "@edition/flow/components/folder/FlowFolderComponent"; import {DataTypeJSONInputTreeComponent} from "@edition/datatype/components/inputs/json/DataTypeJSONInputTreeComponent"; import { EditableJSONEntry } from "../inputs/json/DataTypeJSONInputComponent"; +import {Sizes} from "@code0-tech/pictor/dist/utils"; -export interface LiteralBadgeComponentProps extends Omit { +export interface LiteralBadgeComponentProps extends Omit { value: LiteralValue + size?: Sizes } export const LiteralBadgeComponent: React.FC = (props) => { - const {value, ...rest} = props + const {value, size = "sm", ...rest} = props const truncatedValue = React.useMemo(() => { return truncateText(JSON.stringify(value.value), 75) @@ -24,7 +26,7 @@ export const LiteralBadgeComponent: React.FC = (prop !JSON.stringify(value).includes(truncatedValue) ? ( - + {truncatedValue} @@ -39,7 +41,7 @@ export const LiteralBadgeComponent: React.FC = (prop setCollapsedState={function (path: string[], collapsed: boolean): void { }}/> ) : ( - + {JSON.stringify(value.value)} ) @@ -48,7 +50,7 @@ export const LiteralBadgeComponent: React.FC = (prop ) : ( - + {truncatedValue} ) diff --git a/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx b/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx index 2d20024a..f66422ed 100644 --- a/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx +++ b/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx @@ -73,7 +73,6 @@ export const DataTypeInputControlsComponent: React.FC onSelect?.(suggest)}> diff --git a/src/packages/ce/src/datatype/components/inputs/number/DataTypeNumberInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/number/DataTypeNumberInputComponent.tsx index 265b5fc4..7c1d5293 100644 --- a/src/packages/ce/src/datatype/components/inputs/number/DataTypeNumberInputComponent.tsx +++ b/src/packages/ce/src/datatype/components/inputs/number/DataTypeNumberInputComponent.tsx @@ -24,7 +24,6 @@ export const DataTypeNumberInputComponent: React.FC { if (typeof value === "string") { - console.log(value) formValidation?.setValue?.(value ? {__typename: "LiteralValue", value: !Number.isNaN(Number(value)) ? Number(value) : value} : undefined) onChange?.(value ? {__typename: "LiteralValue", value: !Number.isNaN(Number(value)) ? Number(value) : value} : undefined) } else { diff --git a/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx index 874057cb..b45498f2 100644 --- a/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx +++ b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx @@ -11,7 +11,6 @@ import { ViewportPortal } from "@xyflow/react"; import React from "react"; -import '@xyflow/react/dist/style.css'; import "./FlowBuilderComponent.style.scss" import {FlowBuilderEdgeComponent} from "./FlowBuilderEdgeComponent"; import {Flow, type Namespace, type NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; @@ -28,6 +27,7 @@ import {FlowPanelLayoutComponent} from "@edition/flow/components/panels/FlowPane import {FlowPanelControlComponent} from "@edition/flow/components/panels/FlowPanelControlComponent"; import {FlowPanelUpdateComponent} from "@edition/flow/components/panels/FlowPanelUpdateComponent"; import {FunctionNodeSquareComponent} from "@edition/function/components/nodes/FunctionNodeSquareComponent"; +import {FlowPanelDefinitionComponent} from "@edition/flow/components/panels/FlowPanelDefinitionComponent"; /** * Dynamically layouts a tree of nodes and their parameter nodes for a flow-based editor. @@ -783,8 +783,9 @@ const InternalFlowBuilder: React.FC = (props) => { - + + ) : null} diff --git a/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx index 3c5a770c..cdf28ec5 100644 --- a/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx +++ b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx @@ -1,5 +1,6 @@ import React from "react"; import { + AiGenerateFlowSubscriptionPayload, Flow, LiteralValue, Namespace, @@ -11,12 +12,7 @@ import { import { Badge, Button, - Dialog, - DialogContent, - DialogOverlay, - DialogPortal, - DialogTrigger, - Spacing, + Flex, Text, Tooltip, TooltipContent, @@ -32,89 +28,41 @@ import {SuggestionDialogComponent} from "@edition/function/components/suggestion import {useHotkeys} from "react-hotkeys-hook"; import {useSelectedFunctionNode} from "@edition/function/hooks/FunctionNode.selected.hook"; import {useFunctionSuggestions} from "@edition/function/hooks/Function.suggestion.hook"; -import {useParams} from "next/navigation"; -import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {ModuleService} from "@edition/module/services/Module.service"; -import {IconCheck, IconCopy} from "@tabler/icons-react"; -import {InputWrapper} from "@code0-tech/pictor/dist/components/form/InputWrapper"; -import {useCopyToClipboard} from "@uidotdev/usehooks"; +import {IconArrowBigUp, IconBackspace, IconLetterA, IconLetterQ} from "@tabler/icons-react"; +import {HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger} from "@radix-ui/react-hover-card"; +import 'ldrs/react/ChaoticOrbit.css' +import {AIChatComponent} from "@edition/ai/components/AIChatComponent"; +import {mapAiGenerationFlowToFlowInput} from "@edition/ai/util/AI.flow.mapper"; +import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {useFlowCompareStore} from "@edition/flow/hooks/Flow.compare.hook"; +import {FlowView} from "@edition/flow/services/Flow.view"; export interface FlowPanelControlComponentProps { + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] flowId: Flow['id'] } export const FlowPanelControlComponent: React.FC = (props) => { //props - const {flowId} = props + const {namespaceId, projectId, flowId} = props //services and stores - const params = useParams() const flowService = useService(FlowService) const flowStore = useStore(FlowService) - const flowTypeService = useService(FlowTypeService) - const flowTypeStore = useStore(FlowTypeService) - const projectService = useService(ProjectService) - const projectStore = useStore(ProjectService) - const moduleService = useService(ModuleService) - const moduleStore = useStore(ModuleService) - - const [copiedText, copyToClipboard] = useCopyToClipboard(); - const hasCopiedText = Boolean(copiedText); + const compareFlow = useFlowCompareStore(state => state.flow) + const setCompareFlow = useFlowCompareStore(state => state.setFlow) + const clearCompareFlow = useFlowCompareStore(state => state.clearFlow) + const [, startTransition] = React.useTransition() const [suggestionDialogOpen, setSuggestionDialogOpen] = React.useState(false) const [addNextNodeTooltipOpen, setAddNextNodeTooltipOpen] = React.useState(false) - const namespaceIndex = params.namespaceId as any as number - const projectIndex = params.projectId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` - //memoized values const selectedNode = useSelectedFunctionNode() const result = useFunctionSuggestions() - const flow = React.useMemo( - () => flowService.getById(flowId, { - namespaceId, - projectId - }), - [flowId, flowStore, namespaceId, projectId] - ) - - const project = React.useMemo( - () => projectService.getById(projectId, { - namespaceId - }), - [projectId, namespaceId, projectStore] - ) - - const flowType = React.useMemo( - () => flowTypeService.getById(flow?.type?.id, { - namespaceId, - projectId, - runtimeId: project?.primaryRuntime?.id - }), - [flow?.type?.id, namespaceId, projectId, project?.primaryRuntime?.id, flowTypeStore] - ) - - const module = React.useMemo( - () => moduleService.getById(flowType?.runtimeModule?.id, { - namespaceId: namespaceId, - projectId: projectId, - runtimeId: project?.primaryRuntime?.id - }), - [flowType?.runtimeModule?.id, namespaceId, projectId, project?.primaryRuntime?.id, moduleStore] - ) - - let endpoint = `http://${module?.definitions?.nodes?.[0]?.host}:${module?.definitions?.nodes?.[0]?.port}${module?.definitions?.nodes?.[0]?.endpoint}` - .replace("${{project_slug}}", project?.slug ?? "${{project_slug}}") - - flow?.settings?.nodes?.forEach(setting => { - endpoint = endpoint.replace(`\${{${setting?.flowSettingIdentifier}}}`, setting?.value) - }) - //callbacks const deleteActiveNode = React.useCallback(() => { if (!selectedNode) return @@ -124,6 +72,51 @@ export const FlowPanelControlComponent: React.FC }) }, [selectedNode, flowService, flowStore]) + const onAIData = React.useCallback((payload: AiGenerateFlowSubscriptionPayload) => { + const aiFlow = payload?.flow + if (!aiFlow) return "No flow returned. Try again." + + const currentFlow = flowService.getById(flowId, {namespaceId, projectId}) + if (!currentFlow) return "Flow not found. Try again." + + const currentFlowName = currentFlow.name ?? undefined + const existingNames = (flowService.values({namespaceId, projectId}) ?? []) + .map(f => f.name) + .filter((n): n is string => !!n && n !== currentFlowName) + + const flowInput = mapAiGenerationFlowToFlowInput(aiFlow, {existingNames}) + if (!flowInput) return "Invalid flow type. Try another model." + + const oldFlowSnapshot: FlowView = JSON.parse(JSON.stringify(currentFlow)) + + flowService.flowUpdate({ + flowId: flowId!, + flowInput: flowInput, + }, true).then(result => { + if ((result?.errors?.length ?? 0) <= 0) { + setCompareFlow(oldFlowSnapshot) + addIslandSuccessNotification({message: "Updated flow"}) + } + }) + }, [flowService, flowStore, namespaceId, projectId, flowId, setCompareFlow]) + + const onAcceptAIChanges = React.useCallback(() => { + clearCompareFlow() + }, [clearCompareFlow]) + + const onDiscardAIChanges = React.useCallback(() => { + if (!compareFlow) return + const flowInput = flowService.getPayload(compareFlow) + flowService.flowUpdate({ + flowId: flowId!, + flowInput: flowInput, + }, true).then(result => { + if ((result?.errors?.length ?? 0) <= 0) { + clearCompareFlow() + } + }) + }, [compareFlow, flowService, flowId, clearCompareFlow]) + const addNodeToFlow = React.useCallback((suggestion: NodeFunction | ReferenceValue | LiteralValue | SubFlowValue) => { if (flowId && suggestion.__typename === "NodeFunction" && selectedNode?.id.includes("NodeFunction")) { startTransition(async () => { @@ -136,6 +129,8 @@ export const FlowPanelControlComponent: React.FC } }, [flowId, flowService, flowStore, selectedNode]) + const [aiOpen, setAiOpen] = React.useState(false) + useHotkeys('shift+a', (keyboardEvent) => { if (selectedNode && !selectedNode.data.functionId) setSuggestionDialogOpen(true) else setAddNextNodeTooltipOpen(true) @@ -143,101 +138,114 @@ export const FlowPanelControlComponent: React.FC keyboardEvent.preventDefault() }, [selectedNode]) - return - - + useHotkeys('backspace', (keyboardEvent) => { + if (selectedNode) deleteActiveNode() + else setAddNextNodeTooltipOpen(true) + keyboardEvent.stopPropagation() + keyboardEvent.preventDefault() + }, [selectedNode]) - - - - - - - - Select a node to delete it - - - - - - + + + + Select a node to delete it + + + + + + + + + + Select a node to add a next node + + + + - - - {!selectedNode || !!selectedNode.data.functionId && - Select a node to add a next node - } - - - {module?.definitions?.nodes?.[0] ? ( - - - - - - - - - setting?.flowSettingIdentifier === "httpMethod")?.value ? ( - - {flow?.settings?.nodes?.find(setting => setting?.flowSettingIdentifier === "httpMethod")?.value} - - ) : undefined} - right={ - - - - }> -
- - {endpoint} - -
- -
-
-
-
- ) : (null as any)} -
- - -
+ + + + + + + + + {compareFlow && ( + + + + + + + )} + + + + + + + ; } diff --git a/src/packages/ce/src/flow/components/panels/FlowPanelDefinitionComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelDefinitionComponent.tsx new file mode 100644 index 00000000..040ad542 --- /dev/null +++ b/src/packages/ce/src/flow/components/panels/FlowPanelDefinitionComponent.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import {Panel} from "@xyflow/react"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +import { + Button, + Dialog, + DialogContent, + DialogOverlay, + DialogPortal, + DialogTrigger, + Spacing, + Text, useService, useStore +} from "@code0-tech/pictor"; +import {InputWrapper} from "@code0-tech/pictor/dist/components/form/InputWrapper"; +import {IconCheck, IconCopy, IconPlayerPlay} from "@tabler/icons-react"; +import {useParams} from "next/navigation"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {ModuleService} from "@edition/module/services/Module.service"; +import {useCopyToClipboard} from "@uidotdev/usehooks"; +import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; + +export const FlowPanelDefinitionComponent: React.FC = () => { + + const params = useParams() + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = useStore(FlowTypeService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const moduleService = useService(ModuleService) + const moduleStore = useStore(ModuleService) + + const [copiedText, copyToClipboard] = useCopyToClipboard(); + const hasCopiedText = Boolean(copiedText); + + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const flowIndex = params.flowId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + const flowId: Flow['id'] = `gid://sagittarius/Flow/${flowIndex}` + + const flow = React.useMemo( + () => flowService.getById(flowId, { + namespaceId, + projectId + }), + [flowId, flowStore, namespaceId, projectId] + ) + + const project = React.useMemo( + () => projectService.getById(projectId, { + namespaceId + }), + [projectId, namespaceId, projectStore] + ) + + const flowType = React.useMemo( + () => flowTypeService.getById(flow?.type?.id, { + namespaceId, + projectId, + runtimeId: project?.primaryRuntime?.id + }), + [flow?.type?.id, namespaceId, projectId, project?.primaryRuntime?.id, flowTypeStore] + ) + + const module = React.useMemo( + () => moduleService.getById(flowType?.runtimeModule?.id, { + namespaceId: namespaceId, + projectId: projectId, + runtimeId: project?.primaryRuntime?.id + }), + [flowType?.runtimeModule?.id, namespaceId, projectId, project?.primaryRuntime?.id, moduleStore] + ) + + let endpoint = `http://${module?.definitions?.nodes?.[0]?.host}:${module?.definitions?.nodes?.[0]?.port}${module?.definitions?.nodes?.[0]?.endpoint}` + .replace("${{project_slug}}", project?.slug ?? "${{project_slug}}") + + flow?.settings?.nodes?.forEach(setting => { + endpoint = endpoint.replace(`\${{${setting?.flowSettingIdentifier}}}`, setting?.value) + }) + + return module?.definitions?.nodes?.[0] && + + + + + + + + + + + setting?.flowSettingIdentifier === "httpMethod")?.value ? ( + + {flow?.settings?.nodes?.find(setting => setting?.flowSettingIdentifier === "httpMethod")?.value} + + ) : undefined} + right={ + + + + }> +
+ + {endpoint} + +
+ +
+
+
+
+
+
+ +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx index ca162f86..b2317423 100644 --- a/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx +++ b/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx @@ -1,25 +1,35 @@ import React from "react"; -import {IconLayout, IconLayoutDistributeHorizontal, IconLayoutDistributeVertical} from "@tabler/icons-react"; import {Panel} from "@xyflow/react"; -import { - SegmentedControl, - SegmentedControlItem, Text, - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger -} from "@code0-tech/pictor"; +import {Text, useService, useStore} from "@code0-tech/pictor"; +import {useParams} from "next/navigation"; +import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {FlowService} from "@edition/flow/services/Flow.service"; -export interface FlowPanelLayoutComponentProps { -} +export const FlowPanelLayoutComponent: React.FC = () => { -export const FlowPanelLayoutComponent: React.FC = (props) => { + const params = useParams() + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) - const {} = props + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const flowIndex = params.flowId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + const flowId: Flow['id'] = `gid://sagittarius/Flow/${flowIndex}` + + const flow = React.useMemo( + () => flowService.getById(flowId, { + namespaceId, + projectId + }), + [flowId, flowStore, namespaceId, projectId] + ) return - + {flow?.name} + {/* @@ -56,6 +66,6 @@ export const FlowPanelLayoutComponent: React.FC = - + */} } \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/Flow.compare.hook.ts b/src/packages/ce/src/flow/hooks/Flow.compare.hook.ts new file mode 100644 index 00000000..c34b4f40 --- /dev/null +++ b/src/packages/ce/src/flow/hooks/Flow.compare.hook.ts @@ -0,0 +1,14 @@ +import { create } from 'zustand' +import {FlowView} from "@edition/flow/services/Flow.view"; + +interface FlowCompareState { + flow: FlowView | null + setFlow: (flow: FlowView) => void + clearFlow: () => void +} + +export const useFlowCompareStore = create((setState) => ({ + flow: null, + setFlow: (flow: FlowView) => setState({flow}), + clearFlow: () => setState({flow: null}), +})) \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts b/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts index e8de2d03..deac3544 100644 --- a/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts +++ b/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts @@ -6,6 +6,7 @@ import {FlowService} from "@edition/flow/services/Flow.service"; import {FunctionService} from "@edition/function/services/Function.service"; import {FALLBACK_FUNCTION_PARAMETER_NAME} from "@core/util/fallback-translations"; import {FlowBuilderEdgeDataProps} from "@edition/flow/components/builder/FlowBuilderEdgeComponent"; +import {useFlowCompareStore} from "@edition/flow/hooks/Flow.compare.hook"; // @ts-ignore export const useEdges = (flowId: Flow['id'], namespaceId?: Namespace['id'], projectId?: NamespaceProject['id']): Edge[] => { @@ -14,8 +15,12 @@ export const useEdges = (flowId: Flow['id'], namespaceId?: Namespace['id'], proj const flowStore = useStore(FlowService) const functionService = useService(FunctionService); const functionStore = useStore(FunctionService) + const flowToCompare = useFlowCompareStore(state => state.flow) - const flow = React.useMemo(() => flowService.getById(flowId, {namespaceId, projectId}), [flowId, flowStore]) + const flow = React.useMemo( + () => flowService.getById(flowId, {namespaceId, projectId}), + [flowId, namespaceId, projectId, flowStore, flowService] + ) return React.useMemo(() => { if (!flow) return [] @@ -154,5 +159,5 @@ export const useEdges = (flowId: Flow['id'], namespaceId?: Namespace['id'], proj } return edges - }, [flow?.editedAt, functionStore.length]); + }, [flowStore, flow?.editedAt, flow, flowToCompare, functionStore.length]); }; \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts b/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts index a1db92f6..58ef3098 100644 --- a/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts +++ b/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts @@ -1,11 +1,12 @@ import {Node} from "@xyflow/react"; import type {Flow, Namespace, NamespaceProject, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; -import React, {useState} from "react"; +import React from "react"; import {hashToColor, useService, useStore} from "@code0-tech/pictor"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FunctionService} from "@edition/function/services/Function.service"; import {FunctionNodeComponentProps} from "@edition/function/components/nodes/FunctionNodeComponent"; import {useFlowSchema} from "@edition/flow/hooks/Flow.schema.hook"; +import {useFlowCompareStore} from "@edition/flow/hooks/Flow.compare.hook"; export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], projectId?: NamespaceProject["id"]): Node[] => { @@ -13,28 +14,27 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], const flowStore = useStore(FlowService) const functionService = useService(FunctionService) const functionStore = useStore(FunctionService) - - const [nodes, setNodes] = useState[]>([]) + const flowToCompare = useFlowCompareStore(state => state.flow) const flow = React.useMemo( () => flowService.getById(flowId, {namespaceId, projectId}), - [flowId, flowStore.length, flowService] + [flowId, namespaceId, projectId, flowStore, flowService] ) const flowSchema = useFlowSchema(flowId, namespaceId, projectId) - React.useEffect(() => { - if (!flow) return - if (functionStore.length <= 0) return - if ((flowSchema?.length ?? 0) < (flow.nodes?.nodes?.length ?? 1)) return; + return React.useMemo[]>(() => { + if (!flow) return [] + if (functionStore.length <= 0) return [] - const visited = new Set(); + const nodes: Node[] = [] + const visited = new Set() let groupCounter = 0; let globalIndex = 0; // Trigger node (ID == flow.id -> edge compatible) - setNodes(() => [{ + nodes.push({ id: `${flow.id}`, type: "trigger", position: {x: 0, y: 0}, @@ -45,7 +45,7 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], color: hashToColor(flowId!), schema: flowSchema?.filter(nodeSchema => nodeSchema?.some(schema => !schema.nodeId))?.flat()! }, - }]) + }) const traverse = ( node: NodeFunction, @@ -56,12 +56,12 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], if (!node?.id) return const functionDefinition = functionService.getById(node.functionDefinition?.id) - const nodeId = node.id; + const nodeId = node.id if (!visited.has(nodeId)) { - visited.add(nodeId); + visited.add(nodeId) - setNodes(prevState => [...prevState, { + nodes.push({ id: nodeId, type: functionDefinition && "design" in functionDefinition ? functionDefinition?.design as string : "default", position: {x: 0, y: 0}, @@ -75,15 +75,15 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], color: hashToColor(nodeId), schema: flowSchema?.filter(nodeSchema => nodeSchema?.some(schema => schema.nodeId === node.id))?.flat()! }, - }]) + }) } node.parameters?.nodes?.forEach((param) => { - const value = param?.value; - if (!value || value.__typename !== "SubFlowValue") return; + const value = param?.value + if (!value || value.__typename !== "SubFlowValue") return if (value.functionDefinition?.id) { - setNodes(prevState => [...prevState, { + nodes.push({ id: `${nodeId}-${param.id}`, type: functionDefinition && "design" in functionDefinition ? functionDefinition?.design as string : "square", position: {x: 0, y: 0}, @@ -100,16 +100,16 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], color: hashToColor(value?.startingNodeId ?? value?.functionDefinition?.id ?? ""), schema: [] }, - }]) + }) return } - const groupId = `${nodeId}-group-${groupCounter++}`; + const groupId = `${nodeId}-group-${groupCounter++}` if (!visited.has(groupId)) { - visited.add(groupId); + visited.add(groupId) - setNodes(prevState => [...prevState, { + nodes.push({ id: groupId, type: "group", position: {x: 0, y: 0}, @@ -123,14 +123,12 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], color: hashToColor(value?.startingNodeId ?? ""), schema: [] }, - }]) + }) } - const child = flowService.getNodeById(flowId, value.startingNodeId); - if (child) traverse(child, groupId); - - - }); + const child = flowService.getNodeById(flowId, value.startingNodeId) + if (child) traverse(child, groupId) + }) if (node.nextNodeId) { const next = flowService.getNodeById(flow.id, node.nextNodeId); @@ -142,7 +140,7 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], const start = flowService.getNodeById(flow.id, flow.startingNodeId); if (start) traverse(start); } - }, [flowStore, flow?.editedAt, functionStore.length, flowSchema]); - return nodes -}; \ No newline at end of file + return nodes + }, [flowStore, flow?.editedAt, flow, flowToCompare, functionStore.length, flowSchema, flowId]) +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/Flow.schema.hook.ts b/src/packages/ce/src/flow/hooks/Flow.schema.hook.ts index fd84c45b..59c8b394 100644 --- a/src/packages/ce/src/flow/hooks/Flow.schema.hook.ts +++ b/src/packages/ce/src/flow/hooks/Flow.schema.hook.ts @@ -42,6 +42,8 @@ export const useFlowSchema = ( if (dataTypes.length <= 0) return if (functions.length <= 0) return + let cancelled = false + const triggerSchema = execute({ flow, dataTypes, @@ -58,9 +60,13 @@ export const useFlowSchema = ( }) Promise.all([triggerSchema!, ...schemas!]).then((value) => { + if (cancelled) return setSchema(value as NodeSchema[][]) }) + return () => { + cancelled = true + } }, [functions.length, dataTypes.length, flowStore, flow?.editedAt, flow?.nodes?.nodes?.length]) return schema diff --git a/src/packages/ce/src/flow/pages/FlowOverviewPage.tsx b/src/packages/ce/src/flow/pages/FlowOverviewPage.tsx new file mode 100644 index 00000000..355930a7 --- /dev/null +++ b/src/packages/ce/src/flow/pages/FlowOverviewPage.tsx @@ -0,0 +1,144 @@ +import React from "react"; +import {ResizablePanel} from "@code0-tech/pictor/dist/components/resizable/Resizable"; +import { + AuroraBackground, + Card, + Col, + Flex, + ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {Panel} from "@xyflow/react"; +import {icon, IconString} from "@core/util/icons"; +import {AIChatComponent} from "@edition/ai/components/AIChatComponent"; +import {useParams} from "next/navigation"; +import { + AiGenerateFlowSubscriptionPayload, + Namespace, + NamespaceProject +} from "@code0-tech/sagittarius-graphql-types"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {mapAiGenerationFlowToFlowInput} from "@edition/ai/util/AI.flow.mapper"; +import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; + +const flowTemplates = [ + { + icons: [ + "simple:shopify", + "simple:dhl", + ], + description: "Smart logistics", + prompt: "Create a parcel shipment over DHL API on order receivement from shopify." + }, + { + icons: [ + "simple:discord", + "simple:github", + ], + description: "Smart devops", + prompt: "Create a discord message if a new issue is created in github." + }, +] + +export const FlowOverviewPage: React.FC = () => { + + const params = useParams() + const [prompt, setPrompt] = React.useState("") + + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + const onAIData = React.useCallback((payload: AiGenerateFlowSubscriptionPayload) => { + const aiFlow = payload?.flow + if (!aiFlow) return "No flow returned. Try again." + + const existingNames = (flowService.values({namespaceId, projectId}) ?? []) + .map(f => f.name) + .filter((n): n is string => !!n) + + const flowInput = mapAiGenerationFlowToFlowInput(aiFlow, {existingNames}) + if (!flowInput) return "Invalid flow type. Try another model." + + flowService.flowCreate({ + flow: flowInput, + projectId: projectId, + }).then(result => { + if ((result?.errors?.length ?? 0) <= 0) { + addIslandSuccessNotification({message: "Created flow"}) + } + }) + }, [flowService, flowStore, namespaceId, projectId]) + + return + + + + Good morning, @root
+ Let's automate something. +
+ {/*@ts-ignore*/} + + + + + + {flowTemplates.map(flowTemplate => { + + const displayIcons = flowTemplate.icons.map(i => icon(i as IconString)) + + return setPrompt(flowTemplate.prompt)}> + + + + {displayIcons.map((DisplayIcon) => ( + /* @ts-ignore*/ + + ))} + + + {flowTemplate.description} + + + + + + {flowTemplate.prompt} + + + + + })} + + + + + + + + +
+ + + +
+} \ No newline at end of file diff --git a/src/packages/ce/src/flow/services/Flow.service.ts b/src/packages/ce/src/flow/services/Flow.service.ts index a30d22cb..1cb68294 100644 --- a/src/packages/ce/src/flow/services/Flow.service.ts +++ b/src/packages/ce/src/flow/services/Flow.service.ts @@ -24,6 +24,7 @@ import { } from "@code0-tech/sagittarius-graphql-types"; import {GraphqlClient} from "@core/util/graphql-client"; import flowsQuery from "@edition/flow/services/queries/Flows.query.graphql"; +import flowQuery from "@edition/flow/services/queries/Flow.query.graphql"; import flowCreateMutation from "@edition/flow/services/mutations/Flow.create.mutation.graphql"; import flowDeleteMutation from "@edition/flow/services/mutations/Flow.delete.mutation.graphql"; import flowUpdateMutation from "@edition/flow/services/mutations/Flow.update.mutation.graphql"; @@ -127,8 +128,10 @@ export class FlowService extends ReactiveArrayService({ + query: flowQuery, + variables: { + namespaceId: mutationFlow.project.namespace.id, + projectId: mutationFlow?.project.id, + flowId: mutationFlow.id, + + firstNode: 50, + afterNode: null, + + firstNodeParameter: 50, + afterNodeParameter: null, + + firstSetting: 50, + afterSetting: null, + + firstNodeResult: 50, + afterNodeResult: null + } + }).then(res => { + const flow = res.data?.namespace?.project?.flow + this.add(new View(flow!)) + }) + } else { + mutationFlow.nodes = {nodes: []} + this.add(new View(mutationFlow)) } } @@ -436,7 +464,7 @@ export class FlowService extends ReactiveArrayService { + async flowUpdate(payload: NamespacesProjectsFlowsUpdateInput, force?: boolean): Promise { const flow = this.getById(payload.flowId) @@ -451,9 +479,39 @@ export class FlowService extends ReactiveArrayService f.id === payload.flowId) - flow.updatedAt = result.data.namespacesProjectsFlowsUpdate.flow.updatedAt - flow.editedAt = undefined - this.set(flowIndex, new View(flow)) + + if (force) { + this.client.query({ + query: flowQuery, + fetchPolicy: "network-only", + variables: { + namespaceId: flow?.project?.namespace?.id, + projectId: flow?.project?.id, + flowId: flow?.id, + + firstNode: 50, + afterNode: null, + + firstNodeParameter: 50, + afterNodeParameter: null, + + firstSetting: 50, + afterSetting: null, + + firstNodeResult: 50, + afterNodeResult: null + } + }).then(res => { + const lflow = res.data?.namespace?.project?.flow! as FlowView + lflow.updatedAt = Date.now().toString() + lflow.editedAt = undefined + this.set(flowIndex, new View(lflow!)) + }) + } else { + flow.updatedAt = Date.now().toString() + flow.editedAt = undefined + this.set(flowIndex, new View(flow)) + } } return result.data?.namespacesProjectsFlowsUpdate ?? undefined diff --git a/src/packages/ce/src/flow/services/fragments/Flow.basic.fragment.graphql b/src/packages/ce/src/flow/services/fragments/Flow.basic.fragment.graphql index 04859612..157b9154 100644 --- a/src/packages/ce/src/flow/services/fragments/Flow.basic.fragment.graphql +++ b/src/packages/ce/src/flow/services/fragments/Flow.basic.fragment.graphql @@ -9,6 +9,10 @@ fragment BasicFlow on Flow { project { __typename id + namespace { + __typename + id + } } settings { __typename diff --git a/src/packages/ce/src/flow/services/fragments/Flow.fragment.graphql b/src/packages/ce/src/flow/services/fragments/Flow.fragment.graphql index db441771..d0fbbf2e 100644 --- a/src/packages/ce/src/flow/services/fragments/Flow.fragment.graphql +++ b/src/packages/ce/src/flow/services/fragments/Flow.fragment.graphql @@ -82,6 +82,10 @@ fragment Flow on Flow { project { __typename id + namespace { + __typename + id + } } settings (first: $firstSetting, after: $afterSetting) { __typename diff --git a/src/packages/ce/src/flow/services/queries/Flow.query.graphql b/src/packages/ce/src/flow/services/queries/Flow.query.graphql new file mode 100644 index 00000000..a1187ccc --- /dev/null +++ b/src/packages/ce/src/flow/services/queries/Flow.query.graphql @@ -0,0 +1,27 @@ +#import "@edition/flow/services/fragments/Flow.fragment.graphql" + +query Flows ( + $namespaceId: NamespaceID! + $projectId: NamespaceProjectID! + $flowId: FlowID! + + $firstNode: Int!, + $afterNode: String, + + $firstNodeParameter: Int!, + $afterNodeParameter: String, + + $firstSetting: Int!, + $afterSetting: String, + + $firstNodeResult: Int!, + $afterNodeResult: String +) { + namespace(id: $namespaceId) { + project(id: $projectId) { + flow (id: $flowId) { + ...Flow + } + } + } +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx b/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx index b071de74..12c63e68 100644 --- a/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx +++ b/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx @@ -228,7 +228,7 @@ export const FlowExecutionResultView: React.FC = () => { return - + #{id?.match(/ExecutionResult\/(\d+)$/)?.[1]} @@ -298,7 +298,7 @@ export const FlowExecutionResultView: React.FC = () => { minWidth: "16px", minHeight: "16px", }} - color={hashToColor(item?.data?.payload?.id)}/> + color={hashToColor(item?.data?.payload?.id ?? "")}/> { minWidth: "16px", minHeight: "16px", }} - color={hashToColor(item?.data?.payload?.id)}/> + color={hashToColor(item?.data?.payload?.id ?? "")}/> { <> - {item.type === "node" ? "Parameters" : "Input"} + {item.type === "node" || item.type === "function" ? "Parameters" : "Input"} - {item.type === "node" ? item.data.input?.map((input: ExecutionParameterResult, index: number) => { + {item.type === "node" || item.type === "function" ? item.data.input?.map((input: ExecutionParameterResult, index: number) => { + //TODO: for item.type === function this is wrong const parameter: ParameterDefinition = item?.data?.payload?.functionDefinition?.parameterDefinitions?.nodes?.[index] return
diff --git a/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.style.scss b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.style.scss index 40722baf..5b65cd4c 100644 --- a/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.style.scss +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.style.scss @@ -34,6 +34,6 @@ } &--active { - box-shadow: 0 0 0 1px rgba(variables.$info, .5); + border: 1px solid rgba(variables.$info, .5); } } \ No newline at end of file diff --git a/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.ts b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.ts index dc46bfd2..c3dfc95e 100644 --- a/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.ts +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.ts @@ -7,6 +7,7 @@ export interface FunctionNodeComponentProps extends Record, Com functionId?: FunctionDefinition['id'] flowId: Flow['id'] schema: NodeSchema[] + compareType?: 'added' | 'removed' | 'changed' color: string parentNodeId?: NodeFunction['id'] isParameter?: boolean diff --git a/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx b/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx index 09d9c526..83c133e9 100644 --- a/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx @@ -1,10 +1,10 @@ -import {Handle, Node, NodeProps, Position, useStore} from "@xyflow/react"; +import {Handle, Node, NodeProps, NodeToolbar, Position, useStore} from "@xyflow/react"; import React, {CSSProperties, memo} from "react"; import "./FunctionNodeComponent.style.scss"; import {FunctionNodeComponentProps} from "./FunctionNodeComponent"; -import {Badge, Card, Flex, Text, useService, useStore as usePictorStore} from "@code0-tech/pictor"; +import {Badge, Button, ButtonGroup, Card, Flex, Text, useService, useStore as usePictorStore} from "@code0-tech/pictor"; import {useFlowValidation} from "@edition/flow/hooks/Flow.validation.hook"; -import {IconVariable} from "@tabler/icons-react"; +import {IconBackspace, IconEdit, IconSparkles, IconVariable} from "@tabler/icons-react"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FunctionService} from "@edition/function/services/Function.service"; import {LiteralBadgeComponent} from "@edition/datatype/components/badges/LiteralBadgeComponent"; @@ -147,6 +147,56 @@ export const FunctionNodeDefaultComponent: React.FC + + + + + + + + - @@ -149,57 +156,101 @@ export const FunctionNodeTriggerComponent: React.FC - - - - Flow trigger - - {module?.definitions?.nodes?.[0] && ( - - {endpoint} - - )} - - - - - {flow ? displayMessage : flowType?.names?.[0].content ?? FALLBACK_FLOW_TYPE_NAME} - + return ( + + - { - isReferenced === true ? ( -
+ + + + + + +
- -
- ) : null - } - - + + + + + { + isReferenced === true ? ( +
+ +
+ ) : null + } + + +
+
+ + {flow ? displayMessage : flowType?.names?.[0].content ?? FALLBACK_FLOW_TYPE_NAME} + +
+ ) }) \ No newline at end of file diff --git a/src/packages/ce/src/function/components/suggestion/Suggestion.util.tsx b/src/packages/ce/src/function/components/suggestion/Suggestion.util.tsx index d01cd766..ccc3e50e 100644 --- a/src/packages/ce/src/function/components/suggestion/Suggestion.util.tsx +++ b/src/packages/ce/src/function/components/suggestion/Suggestion.util.tsx @@ -50,9 +50,9 @@ export const useMappedSuggestions = (suggestions: (NodeFunction | SubFlowValue | value: suggestion, icon: functionDefinition?.displayIcon, displayMessage: functionDefinition?.names?.[0].content, - aliases: functionDefinition?.aliases?.[0].content?.split(";"), + aliases: functionDefinition?.aliases?.[0]?.content?.split(";"), definitionSource: module?.identifier, - description: functionDefinition?.descriptions?.[0].content, + description: functionDefinition?.descriptions?.[0]?.content, } } @@ -73,10 +73,10 @@ export const useMappedSuggestions = (suggestions: (NodeFunction | SubFlowValue | return { value: suggestion, icon: suggestion.functionDefinition?.displayIcon, - displayMessage: suggestion.functionDefinition?.names?.[0].content, - aliases: suggestion.functionDefinition?.aliases?.[0].content?.split(";"), + displayMessage: suggestion.functionDefinition?.names?.[0]?.content, + aliases: (suggestion.functionDefinition?.aliases?.[0]?.content ?? "")?.split(";"), definitionSource: suggestion.functionDefinition.runtimeModule?.identifier, - description: suggestion.functionDefinition?.descriptions?.[0].content, + description: suggestion.functionDefinition?.descriptions?.[0]?.content ?? "", } } diff --git a/src/packages/ce/src/function/components/suggestion/SuggestionDialogComponent.tsx b/src/packages/ce/src/function/components/suggestion/SuggestionDialogComponent.tsx index 9818652e..86a15b04 100644 --- a/src/packages/ce/src/function/components/suggestion/SuggestionDialogComponent.tsx +++ b/src/packages/ce/src/function/components/suggestion/SuggestionDialogComponent.tsx @@ -114,7 +114,7 @@ export const SuggestionDialogComponent: React.FC const DisplayIcon = icon(suggestion.icon as IconString) return <> - { - {module?.descriptions?.[0].content}
+ {module?.descriptions?.[0]?.content}
This plugin was developed by {" "} @{module?.author} @@ -160,8 +160,8 @@ export const ModuleConfigurationPage: React.FC = () => { if (!moduleConfigurationSchemas[index]) return null return
- diff --git a/src/packages/ce/src/module/pages/ModulesPage.tsx b/src/packages/ce/src/module/pages/ModulesPage.tsx index bcdeafed..8ac6b205 100644 --- a/src/packages/ce/src/module/pages/ModulesPage.tsx +++ b/src/packages/ce/src/module/pages/ModulesPage.tsx @@ -113,7 +113,7 @@ export const ModulesPage: React.FC = () => { - {module.descriptions?.[0].content}
+ {module.descriptions?.[0]?.content}
This plugin was developed by @{module.author} diff --git a/src/packages/ce/src/project/views/ProjectTabView.tsx b/src/packages/ce/src/project/views/ProjectTabView.tsx index 9e616c66..30416b3a 100644 --- a/src/packages/ce/src/project/views/ProjectTabView.tsx +++ b/src/packages/ce/src/project/views/ProjectTabView.tsx @@ -23,11 +23,7 @@ export const ProjectTabView: React.FC = () => { diff --git a/src/packages/core/src/util/icons.tsx b/src/packages/core/src/util/icons.tsx index 1a6899eb..f79a858c 100644 --- a/src/packages/core/src/util/icons.tsx +++ b/src/packages/core/src/util/icons.tsx @@ -1,11 +1,26 @@ import React from "react" import * as TablerIcons from "@tabler/icons-react" +import * as SimpleIcons from "@icons-pack/react-simple-icons" -export type IconString = `${'tabler'}:${string}` +type TablerIcon = React.FC> +type SimpleIcon = React.FC> +export type IconString = `${'tabler'}:${string}` | `${'simple'}:${string}` -export const icon = (icon?: IconString): React.FC> => { +export const icon = (icon?: IconString): TablerIcon | SimpleIcon => { const fallbackIcon = TablerIcons.IconNote + + if (icon?.startsWith("simple:")) { + const name = icon.replace("simple:", "").trim() + const normalizedName = `Si${name.charAt(0).toUpperCase() + name.slice(1)}` + + const resolvedIcon = normalizedName in SimpleIcons + ? SimpleIcons[normalizedName as keyof typeof SimpleIcons] + : fallbackIcon + + return resolvedIcon as SimpleIcon + } + const normalizedIconName = `Icon${(icon?.replace("tabler:", "") ?? "Note") .trim() .replace(/^icon/i, "") @@ -18,5 +33,5 @@ export const icon = (icon?: IconString): React.FC> + return resolvedIcon as TablerIcon } \ No newline at end of file