├── .editorconfig ├── .env.template ├── .eslintignore ├── .gitignore ├── .prettierignore ├── README.md ├── docs ├── example.gif └── examples │ └── webpage-extract.png ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── favicon.svg ├── index.html ├── logo@2x.png ├── robots.txt └── workflow-examples │ ├── initial.json │ ├── sentiment-analysis.json │ └── webpage-parsing.json ├── src ├── App.tsx ├── components │ ├── editable.tsx │ ├── modal │ │ ├── AddNodeModal.tsx │ │ ├── ConfigureMappingModal.tsx │ │ └── index.tsx │ ├── nodes.tsx │ └── nodes │ │ ├── audioTranscription.tsx │ │ ├── destinations │ │ ├── connection.tsx │ │ └── template.tsx │ │ ├── dropArea.tsx │ │ ├── extract.tsx │ │ ├── loop.tsx │ │ ├── nodeDivider.tsx │ │ ├── sources │ │ ├── connection.tsx │ │ ├── connectionConfig │ │ │ ├── googleSheetConfig.tsx │ │ │ └── hubspotConfig.tsx │ │ ├── file.tsx │ │ ├── index.tsx │ │ ├── static.tsx │ │ └── url.tsx │ │ └── summarize.tsx ├── index.css ├── index.tsx ├── reportWebVitals.ts ├── types │ ├── index.tsx │ ├── node.tsx │ ├── spyglassApi.tsx │ └── typeutils.ts ├── utils │ ├── metrics.tsx │ ├── nodeUtils.ts │ └── workflowValidator.ts └── workflows │ ├── executor.tsx │ ├── index.tsx │ ├── task-executor.tsx │ ├── utils.tsx │ └── workflowinstance.ts ├── tailwind.config.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | # Matches multiple files with brace expansion notation 9 | # Set default charset 10 | [*.{css,tsx,html,json}] 11 | charset = utf-8 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=http://localhost:3000/playground 2 | REACT_APP_API_ENDPOINT=http://localhost:8181/api 3 | REACT_APP_API_TOKEN=auth-token 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | tailwind.config.js 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | package.json 3 | public/ 4 | *.css 5 | tsconfig.json 6 | tailwind.config.js 7 | *.md 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## talos 2 | 3 | Talos is meant to be a powerful interface to easily create automated workflows 4 | that uses large language models (LLMs) to parse, extract, summarize, etc. different 5 | types of content and push it to other APIs/databases/etc. 6 | 7 | Think [Zapier](https://en.wikipedia.org/wiki/Zapier) but with language model magic 🪄. 8 | 9 |

10 | 11 | 12 | 13 |

14 | Click to play video. Note: the summarization step is sped up (~1 minute). 15 | 16 | ## Example Workflows 17 | 18 | ### Read this Wikipedia page & extract some data from it. 19 | 20 | [View workflow](./docs/examples/webpage-extract.png) 21 | 22 | 1. Fetch this wikipedia page. 23 | 2. Extract who the page is about, give a summary, and extract any topics discussed. 24 | 3. Put into a template. 25 | 26 | ### Read a Yelp review & tell me about it. 27 | 28 | 1. Read this Yelp review 29 | 2. Extract the sentiment and give me a list of complaints and/or praises 30 | 3. Put into a report template. 31 | 4. Email it to me. 32 | 33 | ### Summarize this book & generate a book report. 34 | 35 | 1. Read through this PDF file. 36 | 2. Create a bullet point summary of the entire book. 37 | 3. Generate key takeaways from the book 38 | 3a. For each takeaway, elaborate 39 | 4. Combine into a template. 40 | 41 | ## Running Locally 42 | 43 | To start running `talos` locally, install the dependencies 44 | 45 | ```bash 46 | # Copy the environment variables 47 | > cp .env.template .env.local 48 | # Install dependencies 49 | > npm install 50 | # Start the front-end 51 | > npm run start 52 | ``` 53 | 54 | The UI will be available at [http://localhost:3000/playground](http://localhost:3000/playground). 55 | 56 | Now we need to start the backend. 57 | 58 | ### Using w/ memex 59 | 60 | [memex](https://github.com/spyglass-search/memex) is a self-hosted LLM backend & 61 | memory store that exposes basic LLM functionality as a RESTful API. 62 | 63 | You will need to either download a local LLM model to use or add your OpenAI to 64 | the `.env.template` file to get started. 65 | 66 | ```bash 67 | > git clone https://github.com/spyglass-search/memex 68 | > cd memex 69 | > docker-compose up 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spyglass-search/talos/2e691abf83e0e205baf951914c08c20fe72389c5/docs/example.gif -------------------------------------------------------------------------------- /docs/examples/webpage-extract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spyglass-search/talos/2e691abf83e0e205baf951914c08c20fe72389c5/docs/examples/webpage-extract.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@handlebars/parser": "^2.1.0", 7 | "@heroicons/react": "^2.0.18", 8 | "@icons-pack/react-simple-icons": "^9.1.0", 9 | "@tailwindcss/forms": "^0.5.4", 10 | "@tailwindcss/typography": "^0.5.9", 11 | "@testing-library/jest-dom": "^5.17.0", 12 | "@testing-library/react": "^13.4.0", 13 | "@testing-library/user-event": "^13.5.0", 14 | "ajv": "^8.12.0", 15 | "axios": "^1.4.0", 16 | "daisyui": "^3.5.1", 17 | "handlebars": "^4.7.8", 18 | "luxon": "^3.4.0", 19 | "mixpanel-browser": "^2.47.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-scripts": "5.0.1", 23 | "rxjs": "^7.8.1", 24 | "typescript": "^4.9.5", 25 | "url": "^0.11.1", 26 | "web-vitals": "^2.1.4" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "lint": "eslint .", 34 | "fmt": "prettier --write ." 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "@types/ajv": "^1.0.0", 56 | "@types/jest": "^27.5.2", 57 | "@types/json-schema": "^7.0.12", 58 | "@types/luxon": "^3.3.1", 59 | "@types/mixpanel-browser": "^2.47.1", 60 | "@types/node": "^16.18.40", 61 | "@types/react": "^18.2.19", 62 | "@types/react-dom": "^18.2.7", 63 | "prettier": "3.0.1", 64 | "tailwindcss": "^3.3.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spyglass-search/talos/2e691abf83e0e205baf951914c08c20fe72389c5/public/favicon.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 21 | 22 | 31 | talos: AI Workflow Builder 32 | 37 | 38 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spyglass-search/talos/2e691abf83e0e205baf951914c08c20fe72389c5/public/logo@2x.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/workflow-examples/initial.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "f55f17e4-61bd-40b4-8ea9-f07103193ea5", 4 | "label": "Initial Data", 5 | "nodeType": "DataSource", 6 | "data": { 7 | "type": "Text", 8 | "content": "Fireball\nLvl 3 Evocation\n\nCasting Time: 1 action\nRange: 150 feet\nTarget: A point you choose within range\nComponents: V S M (A tiny ball of bat guano and sulfur)\nDuration: Instantaneous\nClasses: Sorcerer, Wizard\nA bright streak flashes from your pointing finger to a point you choose within range and then blossoms with a low roar into an explosion of flame. Each creature in a 20-foot-radius sphere centered on that point must make a Dexterity saving throw. A target takes 8d6 fire damage on a failed save, or half as much damage on a successful one. The fire spreads around corners. It ignites flammable objects in the area that aren’t being worn or carried.\nAt Higher Levels: When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for each slot level above 3rd." 9 | } 10 | }, 11 | { 12 | "uuid": "5c1ea745-b9e4-44c3-a059-6a83f3bada6a", 13 | "label": "Extract Spell Data", 14 | "nodeType": "Extract", 15 | "data": { 16 | "query": "extract the level, name, casting time, classes, range, and damage of this spell", 17 | "schema": { 18 | "$schema": "https://json-schema.org/draft/2020-12/schema", 19 | "type": "object", 20 | "properties": { 21 | "level": { 22 | "type": "number" 23 | }, 24 | "name": { 25 | "type": "string" 26 | }, 27 | "castingTime": { 28 | "type": "string" 29 | }, 30 | "classes": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | } 35 | }, 36 | "range": { 37 | "type": "string" 38 | }, 39 | "damage": { 40 | "type": "string" 41 | } 42 | }, 43 | "required": [ 44 | "name", 45 | "castingTime", 46 | "classes" 47 | ] 48 | } 49 | } 50 | }, 51 | { 52 | "uuid": "template12413", 53 | "label": "Generate Template", 54 | "nodeType": "Template", 55 | "data": { 56 | "template": "{{name}} (Lvl. {{level}}) takes {{action}} and does {{damage}}.\n\nClasses:\n{{#each classes}}\n- {{.}}\n{{/each}}", 57 | "varMapping": { 58 | "name": "name", 59 | "level": "level", 60 | "classes": "classes", 61 | "action": "castingTime", 62 | "damage": "damage" 63 | } 64 | } 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /public/workflow-examples/sentiment-analysis.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "50bffddf-1c43-4d82-b6e8-60633ac4e173", 4 | "label": "Example Yelp Review", 5 | "nodeType": "DataSource", 6 | "data": { 7 | "content": "So disappointed.\nthe food really. I got the pork belly \"carbonara\" and not only was the pork belly the worst I've ever had (a was cold and undercooked), but the rice cakes were even . And maybe it was the yolk that made the rice cakes taste bad but something was off and literally had to gulp down my food with the wine. I was so disappointed; I haven't had such a bad man in such a long time. I had a decision to make at that time, say something or not. And despite being a $28 meal that I absolutely hated, I chickened out and paid the full amount. I know probably the wrong decision but I just couldn't bring myself to have them take it back because I'd feel terrible. I hate doing that. So probably the wrong decision but here we are.\n\nOn the other hand, the service was absolutely fantastic! Everyone was so nice. I greatly appreciated that!\nTo drink I had the cab. Tasty and lighter than most cabs.\n\nI'd probably come back for the wine/drinks. The food? I don't think so", 8 | "type": "Text" 9 | } 10 | }, 11 | { 12 | "uuid": "4a4e7377-4a1f-40f0-bf34-5e1be953d4c4", 13 | "label": "Extract node", 14 | "nodeType": "Extract", 15 | "data": { 16 | "query": "extract the sentiment and complaints from this review", 17 | "schema": { 18 | "$schema": "https://json-schema.org/draft/2020-12/schema", 19 | "type": "object", 20 | "properties": { 21 | "sentiment": { 22 | "type": "string", 23 | "enum": [ 24 | "happy", 25 | "satisfied", 26 | "dissatisfied", 27 | "angry" 28 | ] 29 | }, 30 | "complaints": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | } 35 | } 36 | }, 37 | "required": [ 38 | "complaints", 39 | "sentiment" 40 | ] 41 | } 42 | } 43 | }, 44 | { 45 | "uuid": "0eebb7a3-f2d0-4c47-b394-93d070d21ba5", 46 | "label": "Template node", 47 | "nodeType": "Template", 48 | "data": { 49 | "template": "Review Report\n\nThe customer was: {{sentiment}}\n\nComplaints:\n{{#each complaints}}\n- {{.}}\n{{/each}}", 50 | "varMapping": { 51 | "sentiment": "sentiment", 52 | "complaints": "complaints" 53 | } 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /public/workflow-examples/webpage-parsing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "55f2859f-8fe6-4d5e-b060-5a9255157ed9", 4 | "label": "Fetch Webpage", 5 | "nodeType": "DataSource", 6 | "data": { 7 | "url": "https://en.wikipedia.org/wiki/Katalin_Karik%C3%B3", 8 | "content": "Fireball\nLvl 3 Evocation\n\nCasting Time: 1 action\nRange: 150 feet\nTarget: A point you choose within range\nComponents: V S M (A tiny ball of bat guano and sulfur)\nDuration: Instantaneous\nClasses: Sorcerer, Wizard\nA bright streak flashes from your pointing finger to a point you choose within range and then blossoms with a low roar into an explosion of flame. Each creature in a 20-foot-radius sphere centered on that point must make a Dexterity saving throw. A target takes 8d6 fire damage on a failed save, or half as much damage on a successful one. The fire spreads around corners. It ignites flammable objects in the area that aren’t being worn or carried.\nAt Higher Levels: When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for each slot level above 3rd.", 9 | "type": "Url" 10 | } 11 | }, 12 | { 13 | "uuid": "ae63ce1a-80f6-4660-8998-913db769d58c", 14 | "label": "Parse Page", 15 | "nodeType": "Extract", 16 | "data": { 17 | "query": "extract the page name and any topics talked about ont he page and create a summary", 18 | "schema": { 19 | "$schema": "https://json-schema.org/draft/2020-12/schema", 20 | "type": "object", 21 | "properties": { 22 | "pageTitle": { 23 | "type": "string" 24 | }, 25 | "summary": { 26 | "type": "string" 27 | }, 28 | "topics": { 29 | "type": "array", 30 | "items": { 31 | "type": "string" 32 | } 33 | } 34 | }, 35 | "required": [ 36 | "pageTitle", 37 | "summary", 38 | "topics" 39 | ] 40 | } 41 | } 42 | }, 43 | { 44 | "uuid": "f4fc8611-0207-4fed-949d-e41a21e37727", 45 | "label": "Template node", 46 | "nodeType": "Template", 47 | "data": { 48 | "template": "This page is about: {{ pageTitle}}\n\nHere is a summary:\n{{ summary }}\n\nAnd the topics that are discussed\n{{#each topics}}\n- {{.}}\n{{/each}}", 49 | "varMapping": { 50 | "pageTitle": "pageTitle", 51 | "summary": "summary", 52 | "topics": "topics" 53 | } 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import axios from "axios"; 3 | import { 4 | ArrowDownIcon, 5 | ArrowUpCircleIcon, 6 | DocumentArrowDownIcon, 7 | PlayIcon, 8 | PlusCircleIcon, 9 | TrashIcon, 10 | } from "@heroicons/react/20/solid"; 11 | import { 12 | NodeDef, 13 | NodeResult, 14 | LastRunDetails, 15 | NodeUpdates, 16 | NodeType, 17 | DataNodeType, 18 | ParentDataDef, 19 | OutputDataType, 20 | DataNodeDef, 21 | NodeResultStatus, 22 | NodeState, 23 | } from "./types/node"; 24 | import { NodeComponent, WorkflowResult } from "./components/nodes"; 25 | import { useState } from "react"; 26 | import { 27 | getPreviousUuid, 28 | insertNode, 29 | loadWorkflow, 30 | nodeComesAfter, 31 | removeNode, 32 | saveWorkflow, 33 | } from "./workflows/utils"; 34 | import { cancelExecution } from "./workflows/executor"; 35 | import { ModalType } from "./types"; 36 | import AddNodeModal from "./components/modal/AddNodeModal"; 37 | import { runWorkflow } from "./workflows"; 38 | import { createNodeDefFromType } from "./utils/nodeUtils"; 39 | import { ConfigureMappingModal } from "./components/modal/ConfigureMappingModal"; 40 | import { API_TOKEN } from "./workflows/task-executor"; 41 | import { 42 | InputOutputDefinition, 43 | canConfigureMappings, 44 | generateInputOutputTypes, 45 | } from "./types/typeutils"; 46 | import { DropArea } from "./components/nodes/dropArea"; 47 | import { NodeDivider } from "./components/nodes/nodeDivider"; 48 | import { 49 | ValidationStatus, 50 | WorkflowValidationResult, 51 | validateWorkflow, 52 | } from "./utils/workflowValidator"; 53 | 54 | function AddAction({ onAdd = () => {} }: { onAdd: () => void }) { 55 | return ( 56 |
57 | 61 |
62 | ); 63 | } 64 | 65 | function App() { 66 | // Is a workflow currently running? 67 | let [workflow, setWorkflow] = useState>([]); 68 | let [workflowDataTypes, setWorkflowDataTypes] = useState< 69 | InputOutputDefinition[] 70 | >([]); 71 | let [validationResult, setValidationResult] = useState< 72 | WorkflowValidationResult | undefined 73 | >(undefined); 74 | let [nodeResults, setNodeResults] = useState>( 75 | new Map(), 76 | ); 77 | let [nodeStates, setNodeStates] = useState>(new Map()); 78 | let [dragNDropAfter, setDragNDropAfter] = useState(true); 79 | let [isRunning, setIsRunning] = useState(false); 80 | let [isLoading, setIsLoading] = useState(false); 81 | let [currentNodeRunning, setCurrentNodeRunning] = useState( 82 | null, 83 | ); 84 | let [cachedNodeTypes, setCachedNodeTypes] = useState<{ 85 | [key: string]: InputOutputDefinition; 86 | }>({}); 87 | let [endResult, setEndResult] = useState(null); 88 | let [dragOverUuid, setDragOverUuid] = useState(null); 89 | let [draggedNode, setDraggedNode] = useState(null); 90 | let [inputNode, setInputNode] = useState(null); 91 | let [outputNode, setOutputNode] = useState(null); 92 | let configureMappingModal = useRef(null); 93 | let fileInput = useRef(null); 94 | let exampleSelection = useRef(null); 95 | let addNodeModal = useRef(null); 96 | 97 | let loadExample = async () => { 98 | if (exampleSelection.current) { 99 | let value = (exampleSelection.current as HTMLSelectElement).value; 100 | if (value && value.length > 0 && value.endsWith(".json")) { 101 | await axios 102 | .get>( 103 | `${process.env.PUBLIC_URL}/workflow-examples/${value}`, 104 | ) 105 | .then((resp) => resp.data) 106 | .then((workflow) => setWorkflow(workflow as Array)); 107 | } 108 | } 109 | }; 110 | 111 | const updateNodeDataTypes = ( 112 | newWorkflow: NodeDef[], 113 | cache: { [key: string]: InputOutputDefinition }, 114 | getAuthToken: () => Promise, 115 | ) => { 116 | generateInputOutputTypes(newWorkflow, cache, getAuthToken).then( 117 | (result) => { 118 | const newCache = { ...cachedNodeTypes }; 119 | for (const node of result) { 120 | if ( 121 | node.outputType === OutputDataType.TableResult && 122 | node.outputSchema 123 | ) { 124 | newCache[node.uuid] = node; 125 | } 126 | } 127 | setCachedNodeTypes(newCache); 128 | setWorkflowDataTypes(result); 129 | console.debug("DataType Results", result); 130 | }, 131 | ); 132 | }; 133 | 134 | const internalRunWorkflow = async () => { 135 | setIsRunning(true); 136 | setEndResult(null); 137 | let lastResult = await runWorkflow( 138 | workflow, 139 | (uuid) => { 140 | setCurrentNodeRunning(uuid); 141 | }, 142 | (currentResults) => { 143 | setNodeResults(currentResults); 144 | }, 145 | async () => { 146 | return API_TOKEN ?? ""; 147 | }, 148 | ); 149 | 150 | setEndResult(lastResult); 151 | setIsRunning(false); 152 | setCurrentNodeRunning(null); 153 | }; 154 | 155 | let handleRunWorkflow = async () => { 156 | let newWorkflowValidation = validateWorkflow(workflowDataTypes); 157 | if (newWorkflowValidation.result === ValidationStatus.Failure) { 158 | generateInputOutputTypes(workflow, cachedNodeTypes, getAuthToken).then( 159 | (result) => { 160 | let newWorkflowValidation = validateWorkflow(result); 161 | setValidationResult(newWorkflowValidation); 162 | if (newWorkflowValidation.result === ValidationStatus.Success) { 163 | internalRunWorkflow(); 164 | } else { 165 | setEndResult({ 166 | status: NodeResultStatus.Error, 167 | data: newWorkflowValidation.validationErrors, 168 | }); 169 | } 170 | }, 171 | ); 172 | } else { 173 | internalRunWorkflow(); 174 | } 175 | }; 176 | 177 | let deleteWorkflowNode = (uuid: string) => { 178 | const newCache = { ...cachedNodeTypes }; 179 | delete newCache[uuid]; 180 | setCachedNodeTypes(newCache); 181 | 182 | nodeStates.delete(uuid); 183 | setNodeStates(nodeStates); 184 | 185 | let previousUUID = getPreviousUuid(uuid, workflow); 186 | const newWorkflow = workflow.flatMap((node) => { 187 | if (node.parentNode) { 188 | (node.data as ParentDataDef).actions = ( 189 | node.data as ParentDataDef 190 | ).actions.flatMap((node) => { 191 | if (node.uuid === uuid) { 192 | return []; 193 | } else { 194 | return node; 195 | } 196 | }); 197 | } 198 | if (node.uuid === uuid) { 199 | return []; 200 | } else { 201 | return node; 202 | } 203 | }); 204 | 205 | if (previousUUID) { 206 | clearPreviousMapping(previousUUID, newWorkflow); 207 | } 208 | setWorkflow(newWorkflow); 209 | 210 | updateNodeDataTypes(newWorkflow, cachedNodeTypes, getAuthToken); 211 | }; 212 | 213 | let updateNodeState = (uuid: string, update: NodeState) => { 214 | const newStates = new Map(nodeStates); 215 | newStates.set(uuid, update); 216 | setNodeStates(newStates); 217 | }; 218 | 219 | let updateWorkflow = (uuid: string, updates: NodeUpdates) => { 220 | const newCache = { ...cachedNodeTypes }; 221 | delete newCache[uuid]; 222 | setCachedNodeTypes(newCache); 223 | 224 | const newWorkflow = workflow.map((node) => { 225 | if (node.uuid === uuid) { 226 | return { 227 | ...node, 228 | label: updates.label ?? node.label, 229 | data: updates.data ?? node.data, 230 | mapping: updates.mapping ?? node.mapping, 231 | }; 232 | } else { 233 | return node; 234 | } 235 | }); 236 | setWorkflow(newWorkflow); 237 | 238 | // Want to avoid making the same external request over and over 239 | if ( 240 | (updates.data && 241 | !((updates.data as DataNodeDef).type === DataNodeType.Connection)) || 242 | updates.mapping 243 | ) { 244 | updateNodeDataTypes(newWorkflow, cachedNodeTypes, getAuthToken); 245 | } 246 | }; 247 | 248 | let clearWorkflow = () => { 249 | setNodeResults(new Map()); 250 | setValidationResult(undefined); 251 | setEndResult(null); 252 | setWorkflow( 253 | workflow.map((node) => { 254 | return { ...node, lastRun: undefined }; 255 | }), 256 | ); 257 | }; 258 | 259 | let cancelWorkflow = () => { 260 | setIsRunning(false); 261 | setCurrentNodeRunning(null); 262 | cancelExecution(); 263 | }; 264 | 265 | let onAddNode = (nodeType: NodeType, subType: DataNodeType | null) => { 266 | let newNode = createNodeDefFromType(nodeType, subType); 267 | 268 | if (newNode) { 269 | let newWorkflow = [...workflow, newNode]; 270 | setWorkflow(newWorkflow); 271 | updateNodeDataTypes(newWorkflow, cachedNodeTypes, getAuthToken); 272 | } 273 | }; 274 | 275 | const nodeDropped = (after: boolean, dropUUID: string) => { 276 | if (!draggedNode) { 277 | return; 278 | } 279 | const newWorkflow = [...workflow]; 280 | const dragNode = removeNode(newWorkflow, draggedNode); 281 | if (dragNode) { 282 | insertNode(newWorkflow, after, dropUUID, dragNode); 283 | } 284 | 285 | setValidationResult(undefined); 286 | updateNodeDataTypes(newWorkflow, cachedNodeTypes, getAuthToken); 287 | setDragOverUuid(null); 288 | setDraggedNode(null); 289 | setNodeResults(new Map()); 290 | setEndResult(null); 291 | setWorkflow( 292 | newWorkflow.map((node) => { 293 | return { ...node, lastRun: undefined }; 294 | }), 295 | ); 296 | }; 297 | 298 | const isValidDropSpot = (dropAfter: boolean, spotUUID: string) => { 299 | if (!draggedNode) { 300 | return false; 301 | } 302 | 303 | return ( 304 | spotUUID === dragOverUuid && 305 | dropAfter === dragNDropAfter && 306 | spotUUID !== draggedNode && 307 | (!dropAfter || 308 | (dropAfter && !nodeComesAfter(workflow, spotUUID, draggedNode))) 309 | ); 310 | }; 311 | 312 | const configureMappings = (inputNode: NodeDef, outputNode: NodeDef) => { 313 | setInputNode(inputNode); 314 | setOutputNode(outputNode); 315 | if (configureMappingModal.current) { 316 | configureMappingModal.current.showModal(); 317 | } 318 | }; 319 | 320 | return ( 321 |
322 | 418 |
419 |
420 | {workflow.length > 0 ? ( 421 | 429 | ) : null} 430 |
431 |
432 | {workflow.map((node, idx) => { 433 | return ( 434 |
435 | updateNodeState(node.uuid, state)} 442 | onDelete={deleteWorkflowNode} 443 | onUpdate={(updates) => updateWorkflow(node.uuid, updates)} 444 | onDragUpdate={(uuid) => setDraggedNode(uuid)} 445 | /> 446 | 454 | 463 | 464 |
465 | ); 466 | })} 467 | 469 | addNodeModal.current && 470 | (addNodeModal.current as ModalType).showModal() 471 | } 472 | /> 473 |
474 | {endResult ? ( 475 |
476 | 477 | 481 |
482 | ) : null} 483 |
484 | 0 ? workflow[workflow.length - 1] : null} 487 | onClick={onAddNode} 488 | inLoop={false} 489 | /> 490 | 497 |
498 | ); 499 | } 500 | 501 | function clearPreviousMapping(uuid: string, workflow: NodeDef[]) { 502 | const node = workflow.find((node) => node.uuid === uuid); 503 | if (node) { 504 | node.mapping = []; 505 | } 506 | } 507 | 508 | async function getAuthToken(): Promise { 509 | return `${API_TOKEN}`; 510 | } 511 | 512 | export default App; 513 | -------------------------------------------------------------------------------- /src/components/editable.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon, PencilSquareIcon } from "@heroicons/react/20/solid"; 2 | import { useRef, useState } from "react"; 3 | 4 | interface EditableFieldProps { 5 | label?: string | null; 6 | data: string; 7 | isCode?: boolean; 8 | className?: string; 9 | placeholder?: string; 10 | onChange?: (newValue: string, oldValue: string) => void; 11 | } 12 | 13 | export function EditableTextarea({ 14 | label = null, 15 | data, 16 | isCode, 17 | onChange = () => {}, 18 | }: EditableFieldProps) { 19 | let [isEditing, setIsEditing] = useState(false); 20 | let fieldInput = useRef(null); 21 | 22 | let saveEdit = () => { 23 | if (fieldInput.current) { 24 | let updatedValue = (fieldInput.current as HTMLTextAreaElement).value; 25 | onChange(updatedValue, data); 26 | } 27 | setIsEditing(false); 28 | }; 29 | 30 | let styles = "w-full h-48 text-xs rounded-lg py-2 px-4"; 31 | if (isCode) { 32 | styles = `${styles} font-mono`; 33 | } else { 34 | styles = `${styles} font-sans whitespace-pre-wrap leading-relaxed`; 35 | } 36 | 37 | if (isEditing) { 38 | styles = `${styles} bg-neutral-900`; 39 | } else { 40 | styles = `${styles} bg-base-100`; 41 | } 42 | 43 | return ( 44 |
45 |
46 | {label ?
{label}
: null} 47 | {isEditing ? ( 48 | 52 | ) : ( 53 | 57 | )} 58 |
59 | {isEditing ? ( 60 |