mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-11 23:35:25 -05:00
508 lines
14 KiB
TypeScript
508 lines
14 KiB
TypeScript
import { type ClassValue, clsx } from "clsx";
|
|
import { isEmpty as _isEmpty } from "lodash";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
import { NodeDimension } from "@/app/(platform)/build/components/legacy-builder/Flow/Flow";
|
|
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 (
|
|
"type" in propertySchema &&
|
|
propertySchema.type === "object" &&
|
|
"properties" in propertySchema
|
|
) {
|
|
// Recursively fill defaults for nested objects
|
|
obj[key] = fillObjectDefaultsFromSchema(obj[key] ?? {}, propertySchema);
|
|
} else if ("type" in propertySchema && propertySchema.type === "array") {
|
|
obj[key] ??= [];
|
|
// If the array items are objects, fill their defaults as well
|
|
if (
|
|
Array.isArray(obj[key]) &&
|
|
propertySchema.items &&
|
|
"type" in propertySchema.items &&
|
|
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"
|
|
);
|
|
}
|
|
|
|
function rectanglesOverlap(
|
|
rect1: { x: number; y: number; width: number; height?: number },
|
|
rect2: { x: number; y: number; width: number; height?: number },
|
|
): boolean {
|
|
const x1 = rect1.x,
|
|
y1 = rect1.y,
|
|
w1 = rect1.width,
|
|
h1 = rect1.height ?? 100;
|
|
const x2 = rect2.x,
|
|
y2 = rect2.y,
|
|
w2 = rect2.width,
|
|
h2 = rect2.height ?? 100;
|
|
|
|
// Check if the rectangles do not overlap
|
|
return !(x1 + w1 <= x2 || x1 >= x2 + w2 || y1 + h1 <= y2 || y1 >= y2 + h2);
|
|
}
|
|
|
|
export function findNewlyAddedBlockCoordinates(
|
|
nodeDimensions: NodeDimension,
|
|
newWidth: number,
|
|
margin: number,
|
|
zoom: number,
|
|
) {
|
|
const nodeDimensionArray = Object.values(nodeDimensions);
|
|
|
|
for (let i = nodeDimensionArray.length - 1; i >= 0; i--) {
|
|
const lastNode = nodeDimensionArray[i];
|
|
const lastNodeHeight = lastNode.height ?? 100;
|
|
|
|
// Right of the last node
|
|
let newX = lastNode.x + lastNode.width + margin;
|
|
let newY = lastNode.y;
|
|
let newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };
|
|
|
|
const collisionRight = nodeDimensionArray.some((node) =>
|
|
rectanglesOverlap(newRect, node),
|
|
);
|
|
|
|
if (!collisionRight) {
|
|
return { x: newX, y: newY };
|
|
}
|
|
|
|
// Left of the last node
|
|
newX = lastNode.x - newWidth - margin;
|
|
newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };
|
|
|
|
const collisionLeft = nodeDimensionArray.some((node) =>
|
|
rectanglesOverlap(newRect, node),
|
|
);
|
|
|
|
if (!collisionLeft) {
|
|
return { x: newX, y: newY };
|
|
}
|
|
|
|
// Below the last node
|
|
newX = lastNode.x;
|
|
newY = lastNode.y + lastNodeHeight + margin;
|
|
newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };
|
|
|
|
const collisionBelow = nodeDimensionArray.some((node) =>
|
|
rectanglesOverlap(newRect, node),
|
|
);
|
|
|
|
if (!collisionBelow) {
|
|
return { x: newX, y: newY };
|
|
}
|
|
}
|
|
|
|
// Default position if no space is found
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|