mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
Compare commits
5 Commits
testing-cl
...
fix/blocks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf39d30e4 | ||
|
|
df2d65f93d | ||
|
|
f81ce7376e | ||
|
|
94669a0404 | ||
|
|
eecae2da3c |
@@ -54,6 +54,7 @@
|
|||||||
"@supabase/supabase-js": "2.50.3",
|
"@supabase/supabase-js": "2.50.3",
|
||||||
"@tanstack/react-query": "5.81.5",
|
"@tanstack/react-query": "5.81.5",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
|
"@tanstack/react-virtual": "3.13.12",
|
||||||
"@types/jaro-winkler": "0.2.4",
|
"@types/jaro-winkler": "0.2.4",
|
||||||
"@xyflow/react": "12.8.1",
|
"@xyflow/react": "12.8.1",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
|
|||||||
36
autogpt_platform/frontend/pnpm-lock.yaml
generated
36
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -92,6 +92,9 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: 8.21.3
|
specifier: 8.21.3
|
||||||
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@tanstack/react-virtual':
|
||||||
|
specifier: 3.13.12
|
||||||
|
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@types/jaro-winkler':
|
'@types/jaro-winkler':
|
||||||
specifier: 0.2.4
|
specifier: 0.2.4
|
||||||
version: 0.2.4
|
version: 0.2.4
|
||||||
@@ -2748,10 +2751,19 @@ packages:
|
|||||||
react: '>=16.8'
|
react: '>=16.8'
|
||||||
react-dom: '>=16.8'
|
react-dom: '>=16.8'
|
||||||
|
|
||||||
|
'@tanstack/react-virtual@3.13.12':
|
||||||
|
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
'@tanstack/table-core@8.21.3':
|
'@tanstack/table-core@8.21.3':
|
||||||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12':
|
||||||
|
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||||
|
|
||||||
'@testing-library/dom@10.4.0':
|
'@testing-library/dom@10.4.0':
|
||||||
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
|
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -9982,8 +9994,16 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
'@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.13.12
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
'@tanstack/table-core@8.21.3': {}
|
'@tanstack/table-core@8.21.3': {}
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12': {}
|
||||||
|
|
||||||
'@testing-library/dom@10.4.0':
|
'@testing-library/dom@10.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@@ -11633,8 +11653,8 @@ snapshots:
|
|||||||
'@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||||
@@ -11653,7 +11673,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
@@ -11664,22 +11684,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
unrs-resolver: 1.11.0
|
unrs-resolver: 1.11.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -11690,7 +11710,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { CustomEdge } from "./CustomEdge";
|
|||||||
import ConnectionLine from "./ConnectionLine";
|
import ConnectionLine from "./ConnectionLine";
|
||||||
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
||||||
import { SaveControl } from "@/components/edit/control/SaveControl";
|
import { SaveControl } from "@/components/edit/control/SaveControl";
|
||||||
import { BlocksControl } from "@/components/edit/control/BlocksControl";
|
import { BlocksControl } from "@/components/edit/control/BlocksControl/BlocksControl";
|
||||||
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
|
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { startTutorial } from "./tutorial";
|
import { startTutorial } from "./tutorial";
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
import React, { useState, useMemo } from "react";
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { TextRenderer } from "@/components/ui/render";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { CustomNode } from "@/components/CustomNode";
|
|
||||||
import { beautifyString } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import { Block, BlockUIType, SpecialBlockID } from "@/lib/autogpt-server-api";
|
|
||||||
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
|
||||||
import { IconToyBrick } from "@/components/ui/icons";
|
|
||||||
import { getPrimaryCategoryColor } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
|
||||||
import jaro from "jaro-winkler";
|
|
||||||
|
|
||||||
interface BlocksControlProps {
|
|
||||||
blocks: Block[];
|
|
||||||
addBlock: (
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
hardcodedValues: Record<string, any>,
|
|
||||||
) => void;
|
|
||||||
pinBlocksPopover: boolean;
|
|
||||||
flows: GraphMeta[];
|
|
||||||
nodes: CustomNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A React functional component that displays a control for managing blocks.
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
* @param {Object} BlocksControlProps - The properties for the BlocksControl component.
|
|
||||||
* @param {Block[]} BlocksControlProps.blocks - An array of blocks to be displayed and filtered.
|
|
||||||
* @param {(id: string, name: string) => void} BlocksControlProps.addBlock - A function to call when a block is added.
|
|
||||||
* @returns The rendered BlocksControl component.
|
|
||||||
*/
|
|
||||||
export const BlocksControl: React.FC<BlocksControlProps> = ({
|
|
||||||
blocks,
|
|
||||||
addBlock,
|
|
||||||
pinBlocksPopover,
|
|
||||||
flows,
|
|
||||||
nodes,
|
|
||||||
}) => {
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const graphHasWebhookNodes = nodes.some((n) =>
|
|
||||||
[BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(n.data.uiType),
|
|
||||||
);
|
|
||||||
const graphHasInputNodes = nodes.some(
|
|
||||||
(n) => n.data.uiType == BlockUIType.INPUT,
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredAvailableBlocks = useMemo(() => {
|
|
||||||
const blockList = blocks
|
|
||||||
.filter((b) => b.uiType !== BlockUIType.AGENT)
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
const agentBlockList = flows.map(
|
|
||||||
(flow) =>
|
|
||||||
({
|
|
||||||
id: SpecialBlockID.AGENT,
|
|
||||||
name: flow.name,
|
|
||||||
description:
|
|
||||||
`Ver.${flow.version}` +
|
|
||||||
(flow.description ? ` | ${flow.description}` : ""),
|
|
||||||
categories: [{ category: "AGENT", description: "" }],
|
|
||||||
inputSchema: flow.input_schema,
|
|
||||||
outputSchema: flow.output_schema,
|
|
||||||
staticOutput: false,
|
|
||||||
uiType: BlockUIType.AGENT,
|
|
||||||
uiKey: flow.id,
|
|
||||||
costs: [],
|
|
||||||
hardcodedValues: {
|
|
||||||
graph_id: flow.id,
|
|
||||||
graph_version: flow.version,
|
|
||||||
input_schema: flow.input_schema,
|
|
||||||
output_schema: flow.output_schema,
|
|
||||||
},
|
|
||||||
}) satisfies Block,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates how well a block matches the search query and returns a relevance score.
|
|
||||||
* The scoring algorithm works as follows:
|
|
||||||
* - Returns 1 if no query (all blocks match equally)
|
|
||||||
* - Normalized query for case-insensitive matching
|
|
||||||
* - Returns 3 for exact substring matches in block name (highest priority)
|
|
||||||
* - Returns 2 when all query words appear in the block name (regardless of order)
|
|
||||||
* - Returns 1.X for blocks with names similar to query using Jaro-Winkler distance (X is similarity score)
|
|
||||||
* - Returns 0.5 when all query words appear in the block description (lowest priority)
|
|
||||||
* - Returns 0 for no match
|
|
||||||
*
|
|
||||||
* Higher scores will appear first in search results.
|
|
||||||
*/
|
|
||||||
const matchesSearch = (block: Block, query: string): number => {
|
|
||||||
if (!query) return 1;
|
|
||||||
const normalizedQuery = query.toLowerCase().trim();
|
|
||||||
const queryWords = normalizedQuery.split(/\s+/);
|
|
||||||
const blockName = block.name.toLowerCase();
|
|
||||||
const beautifiedName = beautifyString(block.name).toLowerCase();
|
|
||||||
const description = block.description.toLowerCase();
|
|
||||||
|
|
||||||
// 1. Exact match in name (highest priority)
|
|
||||||
if (
|
|
||||||
blockName.includes(normalizedQuery) ||
|
|
||||||
beautifiedName.includes(normalizedQuery)
|
|
||||||
) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. All query words in name (regardless of order)
|
|
||||||
const allWordsInName = queryWords.every(
|
|
||||||
(word) => blockName.includes(word) || beautifiedName.includes(word),
|
|
||||||
);
|
|
||||||
if (allWordsInName) return 2;
|
|
||||||
|
|
||||||
// 3. Similarity with name (Jaro-Winkler)
|
|
||||||
const similarityThreshold = 0.65;
|
|
||||||
const nameSimilarity = jaro(blockName, normalizedQuery);
|
|
||||||
const beautifiedSimilarity = jaro(beautifiedName, normalizedQuery);
|
|
||||||
const maxSimilarity = Math.max(nameSimilarity, beautifiedSimilarity);
|
|
||||||
if (maxSimilarity > similarityThreshold) {
|
|
||||||
return 1 + maxSimilarity; // Score between 1 and 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. All query words in description (lower priority)
|
|
||||||
const allWordsInDescription = queryWords.every((word) =>
|
|
||||||
description.includes(word),
|
|
||||||
);
|
|
||||||
if (allWordsInDescription) return 0.5;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return blockList
|
|
||||||
.concat(agentBlockList)
|
|
||||||
.map((block) => ({
|
|
||||||
block,
|
|
||||||
score: matchesSearch(block, searchQuery),
|
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
({ block, score }) =>
|
|
||||||
score > 0 &&
|
|
||||||
(!selectedCategory ||
|
|
||||||
block.categories.some((cat) => cat.category === selectedCategory)),
|
|
||||||
)
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.map(({ block }) => ({
|
|
||||||
...block,
|
|
||||||
notAvailable:
|
|
||||||
(block.uiType == BlockUIType.WEBHOOK &&
|
|
||||||
graphHasWebhookNodes &&
|
|
||||||
"Agents can only have one webhook-triggered block") ||
|
|
||||||
(block.uiType == BlockUIType.WEBHOOK &&
|
|
||||||
graphHasInputNodes &&
|
|
||||||
"Webhook-triggered blocks can't be used together with input blocks") ||
|
|
||||||
(block.uiType == BlockUIType.INPUT &&
|
|
||||||
graphHasWebhookNodes &&
|
|
||||||
"Input blocks can't be used together with a webhook-triggered block") ||
|
|
||||||
null,
|
|
||||||
}));
|
|
||||||
}, [
|
|
||||||
blocks,
|
|
||||||
flows,
|
|
||||||
searchQuery,
|
|
||||||
selectedCategory,
|
|
||||||
graphHasInputNodes,
|
|
||||||
graphHasWebhookNodes,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const resetFilters = React.useCallback(() => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setSelectedCategory(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Extract unique categories from blocks
|
|
||||||
const categories = Array.from(
|
|
||||||
new Set([
|
|
||||||
null,
|
|
||||||
...blocks
|
|
||||||
.flatMap((block) => block.categories.map((cat) => cat.category))
|
|
||||||
.sort(),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={pinBlocksPopover ? true : undefined}
|
|
||||||
onOpenChange={(open) => open || resetFilters()}
|
|
||||||
>
|
|
||||||
<Tooltip delayDuration={500}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
data-id="blocks-control-popover-trigger"
|
|
||||||
data-testid="blocks-control-blocks-button"
|
|
||||||
name="Blocks"
|
|
||||||
className="dark:hover:bg-slate-800"
|
|
||||||
>
|
|
||||||
<IconToyBrick />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">Blocks</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<PopoverContent
|
|
||||||
side="right"
|
|
||||||
sideOffset={22}
|
|
||||||
align="start"
|
|
||||||
className="absolute -top-3 w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
|
|
||||||
data-id="blocks-control-popover-content"
|
|
||||||
>
|
|
||||||
<Card className="p-3 pb-0 dark:bg-slate-900">
|
|
||||||
<CardHeader className="flex flex-col gap-x-8 gap-y-1 p-3 px-2">
|
|
||||||
<div className="items-center justify-between">
|
|
||||||
<Label
|
|
||||||
htmlFor="search-blocks"
|
|
||||||
className="whitespace-nowrap text-base font-bold text-black dark:text-white 2xl:text-xl"
|
|
||||||
data-id="blocks-control-label"
|
|
||||||
data-testid="blocks-control-blocks-label"
|
|
||||||
>
|
|
||||||
Blocks
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<MagnifyingGlassIcon className="absolute m-2 h-5 w-5 text-gray-500 dark:text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="search-blocks"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search blocks"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="rounded-lg px-8 py-5 dark:bg-slate-800 dark:text-white"
|
|
||||||
data-id="blocks-control-search-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
|
||||||
{categories.map((category) => {
|
|
||||||
const color = getPrimaryCategoryColor([
|
|
||||||
{ category: category || "All", description: "" },
|
|
||||||
]);
|
|
||||||
const colorClass =
|
|
||||||
selectedCategory === category ? `${color}` : "";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={category}
|
|
||||||
className={`cursor-pointer rounded-xl border px-2 py-2 text-xs font-medium dark:border-slate-700 dark:text-white ${colorClass}`}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedCategory(
|
|
||||||
selectedCategory === category ? null : category,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{beautifyString((category || "All").toLowerCase())}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="overflow-scroll border-t border-t-gray-200 p-0 dark:border-t-slate-700">
|
|
||||||
<ScrollArea
|
|
||||||
className="h-[60vh] w-full"
|
|
||||||
data-id="blocks-control-scroll-area"
|
|
||||||
>
|
|
||||||
{filteredAvailableBlocks.map((block) => (
|
|
||||||
<Card
|
|
||||||
key={block.uiKey || block.id}
|
|
||||||
className={`m-2 my-4 flex h-20 shadow-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 ${
|
|
||||||
block.notAvailable
|
|
||||||
? "cursor-not-allowed opacity-50"
|
|
||||||
: "cursor-pointer hover:shadow-lg"
|
|
||||||
}`}
|
|
||||||
data-id={`block-card-${block.id}`}
|
|
||||||
onClick={() =>
|
|
||||||
!block.notAvailable &&
|
|
||||||
addBlock(block.id, block.name, block?.hardcodedValues || {})
|
|
||||||
}
|
|
||||||
title={block.notAvailable ?? undefined}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`-ml-px h-full w-3 rounded-l-xl ${getPrimaryCategoryColor(block.categories)}`}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div className="mx-3 flex flex-1 items-center justify-between">
|
|
||||||
<div className="mr-2 min-w-0">
|
|
||||||
<span
|
|
||||||
className="block truncate pb-1 text-sm font-semibold dark:text-white"
|
|
||||||
data-id={`block-name-${block.id}`}
|
|
||||||
data-type={block.uiType}
|
|
||||||
data-testid={`block-name-${block.id}`}
|
|
||||||
>
|
|
||||||
<TextRenderer
|
|
||||||
value={beautifyString(block.name).replace(
|
|
||||||
/ Block$/,
|
|
||||||
"",
|
|
||||||
)}
|
|
||||||
truncateLengthLimit={45}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="block break-all text-xs font-normal text-gray-500 dark:text-gray-400"
|
|
||||||
data-testid={`block-description-${block.id}`}
|
|
||||||
>
|
|
||||||
<TextRenderer
|
|
||||||
value={block.description}
|
|
||||||
truncateLengthLimit={165}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex flex-shrink-0 items-center gap-1"
|
|
||||||
data-id={`block-tooltip-${block.id}`}
|
|
||||||
data-testid={`block-add`}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-6 w-6 rounded-lg bg-gray-200 stroke-black stroke-[0.5px] p-1 dark:bg-gray-700 dark:stroke-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { TextRenderer } from "@/components/ui/render";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||||
|
import { IconToyBrick } from "@/components/ui/icons";
|
||||||
|
import { getPrimaryCategoryColor, beautifyString } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Block, GraphMeta } from "@/lib/autogpt-server-api";
|
||||||
|
import { CustomNode } from "@/components/CustomNode";
|
||||||
|
import { useBlocksControl } from "./useBlocksControl";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
blocks: Block[];
|
||||||
|
addBlock: (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
hardcodedValues: Record<string, any>,
|
||||||
|
) => void;
|
||||||
|
pinBlocksPopover: boolean;
|
||||||
|
flows: GraphMeta[];
|
||||||
|
nodes: CustomNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlocksControlComponent({
|
||||||
|
blocks,
|
||||||
|
addBlock,
|
||||||
|
pinBlocksPopover,
|
||||||
|
flows,
|
||||||
|
nodes,
|
||||||
|
}: Props) {
|
||||||
|
const {
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
selectedCategory,
|
||||||
|
filteredAvailableBlocks,
|
||||||
|
categories,
|
||||||
|
resetFilters,
|
||||||
|
handleCategoryClick,
|
||||||
|
handleAddBlock,
|
||||||
|
} = useBlocksControl({
|
||||||
|
blocks,
|
||||||
|
flows,
|
||||||
|
nodes,
|
||||||
|
addBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: filteredAvailableBlocks.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 96, // 80px height + 16px margin (my-4)
|
||||||
|
overscan: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={pinBlocksPopover ? true : undefined}
|
||||||
|
onOpenChange={(open) => open || resetFilters()}
|
||||||
|
>
|
||||||
|
<Tooltip delayDuration={500}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-id="blocks-control-popover-trigger"
|
||||||
|
data-testid="blocks-control-blocks-button"
|
||||||
|
name="Blocks"
|
||||||
|
className="dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<IconToyBrick />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">Blocks</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent
|
||||||
|
side="right"
|
||||||
|
sideOffset={22}
|
||||||
|
align="start"
|
||||||
|
className="absolute -top-3 w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
|
||||||
|
data-id="blocks-control-popover-content"
|
||||||
|
>
|
||||||
|
<Card className="p-3 pb-0 dark:bg-slate-900">
|
||||||
|
<CardHeader className="flex flex-col gap-x-8 gap-y-1 p-3 px-2">
|
||||||
|
<div className="items-center justify-between">
|
||||||
|
<Label
|
||||||
|
htmlFor="search-blocks"
|
||||||
|
className="whitespace-nowrap text-base font-bold text-black dark:text-white 2xl:text-xl"
|
||||||
|
data-id="blocks-control-label"
|
||||||
|
data-testid="blocks-control-blocks-label"
|
||||||
|
>
|
||||||
|
Blocks
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<MagnifyingGlassIcon className="absolute m-2 h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||||
|
<Input
|
||||||
|
id="search-blocks"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search blocks"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="rounded-lg px-8 py-5 dark:bg-slate-800 dark:text-white"
|
||||||
|
data-id="blocks-control-search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{categories.map((category) => {
|
||||||
|
const color = getPrimaryCategoryColor([
|
||||||
|
{ category: category || "All", description: "" },
|
||||||
|
]);
|
||||||
|
const colorClass =
|
||||||
|
selectedCategory === category ? `${color}` : "";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={category}
|
||||||
|
className={`cursor-pointer rounded-xl border px-2 py-2 text-xs font-medium dark:border-slate-700 dark:text-white ${colorClass}`}
|
||||||
|
onClick={() => handleCategoryClick(category)}
|
||||||
|
>
|
||||||
|
{beautifyString((category || "All").toLowerCase())}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="border-t border-t-gray-200 p-0 dark:border-t-slate-700">
|
||||||
|
<div
|
||||||
|
ref={parentRef}
|
||||||
|
className="h-[60vh] w-full overflow-auto"
|
||||||
|
data-id="blocks-control-scroll-area"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualizer
|
||||||
|
.getVirtualItems()
|
||||||
|
.map((virtualItem: VirtualItem) => {
|
||||||
|
const block = filteredAvailableBlocks[virtualItem.index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={virtualItem.key}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: `${virtualItem.size}px`,
|
||||||
|
transform: `translateY(${virtualItem.start}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={`m-2 my-4 flex h-20 shadow-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 ${
|
||||||
|
block.notAvailable
|
||||||
|
? "cursor-not-allowed opacity-50"
|
||||||
|
: "cursor-pointer hover:shadow-lg"
|
||||||
|
}`}
|
||||||
|
data-id={`block-card-${block.id}`}
|
||||||
|
onClick={() => handleAddBlock(block)}
|
||||||
|
title={block.notAvailable ?? undefined}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`-ml-px h-full w-3 rounded-l-xl ${getPrimaryCategoryColor(block.categories)}`}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className="mx-3 flex flex-1 items-center justify-between">
|
||||||
|
<div className="mr-2 min-w-0">
|
||||||
|
<span
|
||||||
|
className="block truncate pb-1 text-sm font-semibold dark:text-white"
|
||||||
|
data-id={`block-name-${block.id}`}
|
||||||
|
data-type={block.uiType}
|
||||||
|
data-testid={`block-name-${block.id}`}
|
||||||
|
>
|
||||||
|
<TextRenderer
|
||||||
|
value={beautifyString(block.name).replace(
|
||||||
|
/ Block$/,
|
||||||
|
"",
|
||||||
|
)}
|
||||||
|
truncateLengthLimit={45}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="block break-all text-xs font-normal text-gray-500 dark:text-gray-400"
|
||||||
|
data-testid={`block-description-${block.id}`}
|
||||||
|
>
|
||||||
|
<TextRenderer
|
||||||
|
value={block.description}
|
||||||
|
truncateLengthLimit={165}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex flex-shrink-0 items-center gap-1"
|
||||||
|
data-id={`block-tooltip-${block.id}`}
|
||||||
|
data-testid={`block-add`}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-6 w-6 rounded-lg bg-gray-200 stroke-black stroke-[0.5px] p-1 dark:bg-gray-700 dark:stroke-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocksControlComponent.displayName = "BlocksControl";
|
||||||
|
export const BlocksControl = React.memo(BlocksControlComponent);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { beautifyString } from "@/lib/utils";
|
||||||
|
import { Block, BlockUIType } from "@/lib/autogpt-server-api";
|
||||||
|
import jaro from "jaro-winkler";
|
||||||
|
|
||||||
|
export interface BlockSearchData {
|
||||||
|
blockName: string;
|
||||||
|
beautifiedName: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnhancedBlock extends Block {
|
||||||
|
searchData: BlockSearchData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockWithAvailability extends Block {
|
||||||
|
notAvailable?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphState {
|
||||||
|
hasWebhookNodes: boolean;
|
||||||
|
hasInputNodes: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlockSearchData(
|
||||||
|
block: Pick<Block, "name" | "description">,
|
||||||
|
): BlockSearchData {
|
||||||
|
return {
|
||||||
|
blockName: block.name.toLowerCase(),
|
||||||
|
beautifiedName: beautifyString(block.name).toLowerCase(),
|
||||||
|
description: block.description.toLowerCase(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesSearch(block: EnhancedBlock, query: string): number {
|
||||||
|
if (!query) return 1;
|
||||||
|
|
||||||
|
const normalizedQuery = query.toLowerCase().trim();
|
||||||
|
const queryWords = normalizedQuery.split(/\s+/);
|
||||||
|
const { blockName, beautifiedName, description } = block.searchData;
|
||||||
|
|
||||||
|
// Exact match in name (highest priority)
|
||||||
|
if (
|
||||||
|
blockName.includes(normalizedQuery) ||
|
||||||
|
beautifiedName.includes(normalizedQuery)
|
||||||
|
) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All query words in name (regardless of order)
|
||||||
|
const allWordsInName = queryWords.every(
|
||||||
|
(word) => blockName.includes(word) || beautifiedName.includes(word),
|
||||||
|
);
|
||||||
|
if (allWordsInName) return 2;
|
||||||
|
|
||||||
|
// Similarity with name (Jaro-Winkler) - Only for short queries to avoid performance issues
|
||||||
|
if (normalizedQuery.length <= 12) {
|
||||||
|
const similarityThreshold = 0.65;
|
||||||
|
const nameSimilarity = jaro(blockName, normalizedQuery);
|
||||||
|
const beautifiedSimilarity = jaro(beautifiedName, normalizedQuery);
|
||||||
|
const maxSimilarity = Math.max(nameSimilarity, beautifiedSimilarity);
|
||||||
|
if (maxSimilarity > similarityThreshold) {
|
||||||
|
return 1 + maxSimilarity; // Score between 1 and 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All query words in description (lower priority)
|
||||||
|
const allWordsInDescription = queryWords.every((word) =>
|
||||||
|
description.includes(word),
|
||||||
|
);
|
||||||
|
if (allWordsInDescription) return 0.5;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlockAvailability(
|
||||||
|
block: Block,
|
||||||
|
graphState: GraphState,
|
||||||
|
): string | null {
|
||||||
|
if (block.uiType === BlockUIType.WEBHOOK && graphState.hasWebhookNodes) {
|
||||||
|
return "Agents can only have one webhook-triggered block";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.uiType === BlockUIType.WEBHOOK && graphState.hasInputNodes) {
|
||||||
|
return "Webhook-triggered blocks can't be used together with input blocks";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.uiType === BlockUIType.INPUT && graphState.hasWebhookNodes) {
|
||||||
|
return "Input blocks can't be used together with a webhook-triggered block";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractCategories(blocks: Block[]): (string | null)[] {
|
||||||
|
return Array.from(
|
||||||
|
new Set([
|
||||||
|
null,
|
||||||
|
...blocks
|
||||||
|
.flatMap((block) => block.categories.map((cat) => cat.category))
|
||||||
|
.sort(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useState, useMemo, useRef, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Block,
|
||||||
|
BlockUIType,
|
||||||
|
SpecialBlockID,
|
||||||
|
GraphMeta,
|
||||||
|
} from "@/lib/autogpt-server-api";
|
||||||
|
import { CustomNode } from "@/components/CustomNode";
|
||||||
|
import {
|
||||||
|
getBlockSearchData,
|
||||||
|
matchesSearch,
|
||||||
|
getBlockAvailability,
|
||||||
|
extractCategories,
|
||||||
|
EnhancedBlock,
|
||||||
|
BlockWithAvailability,
|
||||||
|
GraphState,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
blocks: Block[];
|
||||||
|
flows: GraphMeta[];
|
||||||
|
nodes: CustomNode[];
|
||||||
|
addBlock: (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
hardcodedValues: Record<string, any>,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBlocksControl({ blocks, flows, nodes, addBlock }: Args) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
|
const animationFrameRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// Memoize graph state checks to avoid recalculating on every render
|
||||||
|
const graphState = useMemo(
|
||||||
|
(): GraphState => ({
|
||||||
|
hasWebhookNodes: nodes.some((n) =>
|
||||||
|
[BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(
|
||||||
|
n.data.uiType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hasInputNodes: nodes.some((n) => n.data.uiType === BlockUIType.INPUT),
|
||||||
|
}),
|
||||||
|
[nodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize blocks with precomputed search data
|
||||||
|
const blocksWithSearchData = useMemo((): EnhancedBlock[] => {
|
||||||
|
return blocks.map((block) => ({
|
||||||
|
...block,
|
||||||
|
searchData: getBlockSearchData(block),
|
||||||
|
}));
|
||||||
|
}, [blocks]);
|
||||||
|
|
||||||
|
// Memoize agent blocks list with search data
|
||||||
|
const agentBlocksWithSearchData = useMemo((): EnhancedBlock[] => {
|
||||||
|
return flows.map((flow) => {
|
||||||
|
const description = `Ver.${flow.version}${flow.description ? ` | ${flow.description}` : ""}`;
|
||||||
|
return {
|
||||||
|
id: SpecialBlockID.AGENT,
|
||||||
|
name: flow.name,
|
||||||
|
description,
|
||||||
|
categories: [{ category: "AGENT", description: "" }],
|
||||||
|
inputSchema: flow.input_schema,
|
||||||
|
outputSchema: flow.output_schema,
|
||||||
|
staticOutput: false,
|
||||||
|
uiType: BlockUIType.AGENT,
|
||||||
|
uiKey: flow.id,
|
||||||
|
costs: [],
|
||||||
|
hardcodedValues: {
|
||||||
|
graph_id: flow.id,
|
||||||
|
graph_version: flow.version,
|
||||||
|
input_schema: flow.input_schema,
|
||||||
|
output_schema: flow.output_schema,
|
||||||
|
},
|
||||||
|
searchData: getBlockSearchData({ name: flow.name, description }),
|
||||||
|
} satisfies EnhancedBlock;
|
||||||
|
});
|
||||||
|
}, [flows]);
|
||||||
|
|
||||||
|
// Memoize filtered and sorted blocks
|
||||||
|
const filteredAvailableBlocks = useMemo((): BlockWithAvailability[] => {
|
||||||
|
const blockList = blocksWithSearchData
|
||||||
|
.filter((b) => b.uiType !== BlockUIType.AGENT)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const allBlocks = blockList.concat(agentBlocksWithSearchData);
|
||||||
|
|
||||||
|
return allBlocks
|
||||||
|
.map((block) => ({
|
||||||
|
block,
|
||||||
|
score: matchesSearch(block, searchQuery),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
({ block, score }) =>
|
||||||
|
score > 0 &&
|
||||||
|
(!selectedCategory ||
|
||||||
|
block.categories.some((cat) => cat.category === selectedCategory)),
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.map(({ block }) => ({
|
||||||
|
...block,
|
||||||
|
notAvailable: getBlockAvailability(block, graphState),
|
||||||
|
}));
|
||||||
|
}, [
|
||||||
|
blocksWithSearchData,
|
||||||
|
agentBlocksWithSearchData,
|
||||||
|
searchQuery,
|
||||||
|
selectedCategory,
|
||||||
|
graphState,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const categories = useMemo(() => extractCategories(blocks), [blocks]);
|
||||||
|
|
||||||
|
// Create requestAnimationFrame-based search query setter
|
||||||
|
const debouncedSetSearchQuery = useCallback((value: string) => {
|
||||||
|
if (animationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
}
|
||||||
|
animationFrameRef.current = requestAnimationFrame(() => {
|
||||||
|
setSearchQuery(value);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
setSearchQuery("");
|
||||||
|
setSelectedCategory(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCategoryClick(category: string | null) {
|
||||||
|
setSelectedCategory(selectedCategory === category ? null : category);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddBlock(block: BlockWithAvailability) {
|
||||||
|
if (!block.notAvailable) {
|
||||||
|
addBlock(block.id, block.name, block?.hardcodedValues || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery: debouncedSetSearchQuery,
|
||||||
|
selectedCategory,
|
||||||
|
filteredAvailableBlocks,
|
||||||
|
categories,
|
||||||
|
resetFilters,
|
||||||
|
handleCategoryClick,
|
||||||
|
handleAddBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user