Files
AutoGPT/autogpt_platform/frontend/src/lib/utils.ts
Abhimanyu Yadav 0a1591fce2 refactor(frontend): remove old builder code and monitoring components
(#12082)

### Changes 🏗️

This PR removes old builder code and monitoring components as part of
the migration to the new flow editor:

- **NewControlPanel**: Simplified component by removing unused props
(`flowExecutionID`, `visualizeBeads`, `pinSavePopover`,
`pinBlocksPopover`, `nodes`, `onNodeSelect`, `onNodeHover`) and cleaned
up commented legacy code
- **Import paths**: Updated all references from
`legacy-builder/CustomNode` to `FlowEditor/nodes/CustomNode`
- **GraphContent**: Fixed type safety by properly handling
`customized_name` metadata and using `categoryColorMap` instead of
`getPrimaryCategoryColor`
- **useNewControlPanel**: Removed unused state and query parameter
handling related to old builder
- Removed dead code and commented-out imports throughout

### Checklist 📋

#### For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
    - [x] Verify NewControlPanel renders correctly
    - [x] Test BlockMenu functionality
    - [x] Test Save Control
    - [x] Test Undo/Redo buttons
    - [x] Verify graph search menu still works with updated imports

<!-- greptile_comment -->

<h2>Greptile Overview</h2>

<details><summary><h3>Greptile Summary</h3></summary>

This PR removes legacy builder components and monitoring page (~12,000
lines of code), simplifying `NewControlPanel` to focus only on the new
flow editor.

**Key changes:**
- Removed entire `legacy-builder/` directory (36 files) containing old
CustomNode, CustomEdge, Flow, and control components
- Deleted `/monitoring` page and all related components (9 files)
- Deleted `useAgentGraph` hook (1,043 lines) that was only used by
legacy components
- Simplified `NewControlPanel` by removing unused props
(`flowExecutionID`, `nodes`, `onNodeSelect`, etc.) and commented-out
code
- Updated imports in `NewSearchGraph` components to reference new
`FlowEditor/nodes/CustomNode` instead of deleted
`legacy-builder/CustomNode`
- Removed `/monitoring` from protected pages in `helpers.ts`
- Updated test files to remove monitoring-related test helpers

**Minor style issues:**
- `useNewControlPanel` hook returns unused state (`blockMenuSelected`)
that should be cleaned up
- Unnecessary double negation (`!!`) in `GraphContent.tsx:136`
</details>


<details><summary><h3>Confidence Score: 4/5</h3></summary>

- This PR is safe to merge with minor style improvements recommended
- The refactor is a straightforward deletion of legacy code with no
references remaining in the codebase. All imports have been updated
correctly, tests cleaned up, and routing configuration updated. The only
issues are minor unused code that could be cleaned up but won't cause
runtime errors.
- No files require special attention - the unused state in
`useNewControlPanel.ts` is a minor style issue
</details>


<details><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
    participant User
    participant NewControlPanel
    participant BlockMenu
    participant NewSaveControl
    participant UndoRedoButtons
    participant Store as blockMenuStore (Zustand)

    Note over NewControlPanel: Simplified component (removed props & legacy code)
    
    User->>NewControlPanel: Render
    NewControlPanel->>useNewControlPanel: Call hook (unused return)
    
    NewControlPanel->>BlockMenu: Render
    BlockMenu->>Store: Access state via useBlockMenuStore
    Store-->>BlockMenu: Return search, filters, etc.
    
    NewControlPanel->>NewSaveControl: Render
    NewControlPanel->>UndoRedoButtons: Render
    
    Note over NewControlPanel,Store: State management moved from hook to Zustand store
    Note over User: Legacy components (CustomNode, Flow, etc.) completely removed
```
</details>


<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->
2026-02-20 05:19:08 +00:00

429 lines
12 KiB
TypeScript

import { type ClassValue, clsx } from "clsx";
import _isEmpty from "lodash/isEmpty";
import { twMerge } from "tailwind-merge";
import {
BlockIOObjectSubSchema,
BlockIORootSchema,
BlockIOSubSchema,
Category,
GraphInputSubSchema,
GraphOutputSubSchema,
} from "@/lib/autogpt-server-api/types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/** Derived from https://stackoverflow.com/a/7616484 */
export function hashString(str: string): number {
let hash = 0,
chr: number;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
/** Derived from https://stackoverflow.com/a/32922084 */
export function deepEquals(x: any, y: any): boolean {
const ok = (obj: any) => Object.keys(obj).filter((key) => obj[key] !== null),
tx = typeof x,
ty = typeof y;
const res =
x && y && tx === ty && tx === "object"
? ok(x).length === ok(y).length &&
ok(x).every((key) => deepEquals(x[key], y[key]))
: x === y;
return res;
}
/** Get tailwind text color class from type name */
export function getTypeTextColor(type: string | null): string {
if (type === null) return "text-gray-500";
return (
{
string: "text-green-500",
number: "text-blue-500",
integer: "text-blue-500",
boolean: "text-yellow-500",
object: "text-purple-500",
array: "text-indigo-500",
null: "text-gray-500",
any: "text-gray-500",
"": "text-gray-500",
}[type] || "text-gray-500"
);
}
/** Get tailwind bg color class from type name */
export function getTypeBgColor(type: string | null): string {
if (type === null) return "border-gray-500";
return (
{
string: "border-green-500",
number: "border-blue-500",
integer: "border-blue-500",
boolean: "border-yellow-500",
object: "border-purple-500",
array: "border-indigo-500",
null: "border-gray-500",
any: "border-gray-500",
"": "border-gray-500",
}[type] || "border-gray-500"
);
}
export function getTypeColor(type: string | undefined): string {
if (!type) return "#6b7280";
return (
{
string: "#22c55e",
number: "#3b82f6",
integer: "#3b82f6",
boolean: "#eab308",
object: "#a855f7",
array: "#6366f1",
null: "#6b7280",
any: "#6b7280",
}[type] || "#6b7280"
);
}
/**
* Extracts the effective type from a JSON schema, handling anyOf/oneOf/allOf wrappers.
* Returns the first non-null type found in the schema structure.
*/
export function getEffectiveType(
schema:
| BlockIOSubSchema
| GraphInputSubSchema
| GraphOutputSubSchema
| null
| undefined,
): string | undefined {
if (!schema) return undefined;
// Direct type property
if ("type" in schema && schema.type) {
return String(schema.type);
}
// Handle allOf - typically a single-item wrapper
if (
"allOf" in schema &&
Array.isArray(schema.allOf) &&
schema.allOf.length > 0
) {
return getEffectiveType(schema.allOf[0]);
}
// Handle anyOf - e.g. [{ type: "string" }, { type: "null" }]
if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
for (const item of schema.anyOf) {
if ("type" in item && item.type !== "null") {
return String(item.type);
}
}
}
// Handle oneOf
if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
for (const item of schema.oneOf) {
if ("type" in item && item.type !== "null") {
return String(item.type);
}
}
}
return undefined;
}
export function beautifyString(name: string): string {
// Regular expression to identify places to split, considering acronyms
const result = name
.replace(/([a-z])([A-Z])/g, "$1 $2") // Add space before capital letters
.replace(/([A-Z])([A-Z][a-z])/g, "$1 $2") // Add space between acronyms and next word
.replace(/_/g, " ") // Replace underscores with spaces
.replace(/\b\w/g, (char) => char.toUpperCase()); // Capitalize the first letter of each word
return applyExceptions(result);
}
const exceptionMap: Record<string, string> = {
"Auto GPT": "AutoGPT",
Gpt: "GPT",
Creds: "Credentials",
Id: "ID",
Openai: "OpenAI",
Api: "API",
Url: "URL",
Http: "HTTP",
Json: "JSON",
Ai: "AI",
"You Tube": "YouTube",
};
const applyExceptions = (str: string): string => {
Object.keys(exceptionMap).forEach((key) => {
const regex = new RegExp(`\\b${key}\\b`, "g");
str = str.replace(regex, exceptionMap[key]);
});
return str;
};
export function exportAsJSONFile(obj: object, filename: string): void {
// Create downloadable blob
const jsonString = JSON.stringify(obj, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
// Trigger the browser to download the blob to a file
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up
URL.revokeObjectURL(url);
}
export function setNestedProperty(obj: any, path: string, value: any) {
if (!obj || typeof obj !== "object") {
throw new Error("Target must be a non-null object");
}
if (!path || typeof path !== "string") {
throw new Error("Path must be a non-empty string");
}
// Split by both / and . to handle mixed separators, then filter empty strings
const keys = path.split(/[\/.]/).filter((key) => key.length > 0);
if (keys.length === 0) {
throw new Error("Path must be a non-empty string");
}
// Validate keys for prototype pollution protection
for (const key of keys) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
throw new Error(`Invalid property name: ${key}`);
}
}
// Securely traverse and set nested properties
// Use Object.prototype.hasOwnProperty.call() to safely check properties
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
// Use hasOwnProperty check to avoid prototype chain access
if (!Object.prototype.hasOwnProperty.call(current, key)) {
current[key] = {};
} else if (typeof current[key] !== "object" || current[key] === null) {
current[key] = {};
}
current = current[key];
}
// Set the final value using bracket notation with validated key
// Since we've validated all keys, this is safe from prototype pollution
const finalKey = keys[keys.length - 1];
current[finalKey] = value;
}
export function pruneEmptyValues(
obj: any,
removeEmptyStrings: boolean = true,
): any {
if (Array.isArray(obj)) {
// If obj is an array, recursively check each element,
// but element removal is avoided to prevent index changes.
return obj.map((item) =>
item === undefined || item === null
? ""
: pruneEmptyValues(item, removeEmptyStrings),
);
} else if (typeof obj === "object" && obj !== null) {
// If obj is an object, recursively remove empty strings and nulls from its properties
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (
value === null ||
value === undefined ||
(typeof value === "string" && value === "" && removeEmptyStrings)
) {
delete obj[key];
} else if (typeof value === "object") {
obj[key] = pruneEmptyValues(value, removeEmptyStrings);
}
}
}
return obj;
}
export function fillObjectDefaultsFromSchema(
obj: Record<any, any>,
schema: BlockIORootSchema | BlockIOObjectSubSchema,
) {
for (const key in schema.properties) {
if (!schema.properties.hasOwnProperty(key)) continue;
const propertySchema = schema.properties[key];
if ("default" in propertySchema && propertySchema.default !== undefined) {
// Apply simple default values
obj[key] ??= propertySchema.default;
} else if (
propertySchema.type === "object" &&
"properties" in propertySchema
) {
// Recursively fill defaults for nested objects
obj[key] = fillObjectDefaultsFromSchema(obj[key] ?? {}, propertySchema);
} else if (propertySchema.type === "array") {
obj[key] ??= [];
// If the array items are objects, fill their defaults as well
if (
Array.isArray(obj[key]) &&
propertySchema.items?.type === "object" &&
"properties" in propertySchema.items
) {
for (const item of obj[key]) {
if (typeof item === "object" && item !== null) {
fillObjectDefaultsFromSchema(item, propertySchema.items);
}
}
}
}
}
return obj;
}
export const categoryColorMap: Record<string, string> = {
AI: "bg-orange-300 dark:bg-orange-700",
SOCIAL: "bg-yellow-300 dark:bg-yellow-700",
TEXT: "bg-green-300 dark:bg-green-700",
SEARCH: "bg-blue-300 dark:bg-blue-700",
BASIC: "bg-purple-300 dark:bg-purple-700",
INPUT: "bg-cyan-300 dark:bg-cyan-700",
OUTPUT: "bg-red-300 dark:bg-red-700",
LOGIC: "bg-teal-300 dark:bg-teal-700",
DEVELOPER_TOOLS: "bg-fuchsia-300 dark:bg-fuchsia-700",
AGENT: "bg-lime-300 dark:bg-lime-700",
};
export function getPrimaryCategoryColor(categories: Category[]): string {
if (categories.length === 0) {
return "bg-gray-300 dark:bg-slate-700";
}
return (
categoryColorMap[categories[0].category] || "bg-gray-300 dark:bg-slate-700"
);
}
export function hasNonNullNonObjectValue(obj: any): boolean {
if (obj !== null && typeof obj === "object") {
return Object.values(obj).some((value) => hasNonNullNonObjectValue(value));
} else {
return obj !== null && typeof obj !== "object";
}
}
type ParsedKey = { key: string; index?: number };
export function parseKeys(key: string): ParsedKey[] {
const splits = key.split(/_@_|_#_|_\$_|\./);
const keys: ParsedKey[] = [];
let currentKey: string | null = null;
splits.forEach((split) => {
const isInteger = /^\d+$/.test(split);
if (!isInteger) {
if (currentKey !== null) {
keys.push({ key: currentKey });
}
currentKey = split;
} else {
if (currentKey !== null) {
keys.push({ key: currentKey, index: parseInt(split, 10) });
currentKey = null;
} else {
throw new Error("Invalid key format: array index without a key");
}
}
});
if (currentKey !== null) {
keys.push({ key: currentKey });
}
return keys;
}
/**
* Get the value of a nested key in an object, handles arrays and objects.
*/
export function getValue(key: string, value: any) {
const keys = parseKeys(key);
return keys.reduce((acc, k) => {
if (acc === undefined) return undefined;
if (k.index !== undefined) {
return Array.isArray(acc[k.key]) ? acc[k.key][k.index] : undefined;
}
return acc[k.key];
}, value);
}
/** Check if a string is empty or whitespace */
export function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0;
}
export function isEmpty(value: any): boolean {
return (
value === undefined ||
value === "" ||
(typeof value === "object" &&
(value instanceof Date ? isNaN(value.getTime()) : _isEmpty(value))) ||
(typeof value === "number" && isNaN(value))
);
}
/** Check if a value is an object or not */
export function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Validate YouTube URL */
export function validateYouTubeUrl(val: string): boolean {
if (!val) return true;
try {
const url = new URL(val);
const allowedHosts = [
"youtube.com",
"www.youtube.com",
"youtu.be",
"www.youtu.be",
];
return allowedHosts.includes(url.hostname);
} catch {
return false;
}
}
export function isValidUUID(value: string): boolean {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}