mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
Add help system and field configuration components
- Introduced a new help system with styled help icons and panels for user guidance. - Created FieldWithHelp component to integrate help text and examples for various input fields. - Added comprehensive field configurations for different node types, enhancing user experience and clarity. - Implemented tests for dual-label system to ensure proper configuration and validation of node types and fields. - Updated NodeConfiguration and NodeLibrary components to utilize new help features and improve accessibility.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { getNodeTypeConfig, nodeTypeConfigs } from "../config/nodeTypeConfigs";
|
||||
import {
|
||||
getFieldConfig,
|
||||
fieldConfigs,
|
||||
getFieldsForNodeType,
|
||||
} from "../config/fieldConfigs";
|
||||
import { NodeType } from "../types";
|
||||
|
||||
describe("Dual-Label System", () => {
|
||||
describe("Node Type Configurations", () => {
|
||||
test("should have configurations for all node types", () => {
|
||||
const nodeTypes: NodeType[] = [
|
||||
"dataInput",
|
||||
"webScraping",
|
||||
"llmTask",
|
||||
"embeddingGenerator",
|
||||
"similaritySearch",
|
||||
"structuredOutput",
|
||||
"dataOutput",
|
||||
"databaseQuery",
|
||||
"databaseInsert",
|
||||
"databaseUpdate",
|
||||
"databaseDelete",
|
||||
];
|
||||
|
||||
nodeTypes.forEach((nodeType) => {
|
||||
const config = getNodeTypeConfig(nodeType);
|
||||
expect(config).toBeDefined();
|
||||
expect(config.userFriendlyName).toBeTruthy();
|
||||
expect(config.technicalName).toBeTruthy();
|
||||
expect(config.description).toBeTruthy();
|
||||
expect(config.helpText).toBeTruthy();
|
||||
expect(config.commonUseCases).toBeInstanceOf(Array);
|
||||
expect(config.commonUseCases.length).toBeGreaterThan(0);
|
||||
expect(config.icon).toBeTruthy();
|
||||
expect(config.category).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("should have user-friendly names that are different from technical names", () => {
|
||||
Object.entries(nodeTypeConfigs).forEach(([nodeType, config]) => {
|
||||
expect(config.userFriendlyName).not.toBe(config.technicalName);
|
||||
expect(config.userFriendlyName).toBeTruthy();
|
||||
expect(config.technicalName).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("should have meaningful help text", () => {
|
||||
Object.entries(nodeTypeConfigs).forEach(([nodeType, config]) => {
|
||||
expect(config.helpText.length).toBeGreaterThan(10);
|
||||
expect(config.helpText).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Field Configurations", () => {
|
||||
const testFields = [
|
||||
"dataType",
|
||||
"prompt",
|
||||
"url",
|
||||
"model",
|
||||
"temperature",
|
||||
"maxTokens",
|
||||
"label",
|
||||
"formats",
|
||||
"maxLength",
|
||||
"query",
|
||||
"table",
|
||||
"format",
|
||||
"filename",
|
||||
];
|
||||
|
||||
test("should have configurations for common fields", () => {
|
||||
testFields.forEach((fieldName) => {
|
||||
const config = getFieldConfig(fieldName);
|
||||
expect(config).toBeDefined();
|
||||
expect(config.userFriendlyName).toBeTruthy();
|
||||
expect(config.technicalName).toBeTruthy();
|
||||
expect(config.description).toBeTruthy();
|
||||
expect(config.helpText).toBeTruthy();
|
||||
expect(config.examples).toBeInstanceOf(Array);
|
||||
expect(config.examples.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("should have user-friendly field names", () => {
|
||||
testFields.forEach((fieldName) => {
|
||||
const config = getFieldConfig(fieldName);
|
||||
if (config) {
|
||||
expect(config.userFriendlyName).not.toBe(config.technicalName);
|
||||
expect(config.userFriendlyName).toMatch(/^[A-Z]/); // Should start with capital letter
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("should have helpful examples for each field", () => {
|
||||
testFields.forEach((fieldName) => {
|
||||
const config = getFieldConfig(fieldName);
|
||||
if (config) {
|
||||
expect(config.examples.length).toBeGreaterThan(0);
|
||||
config.examples.forEach((example) => {
|
||||
expect(example).toBeTruthy();
|
||||
expect(example.length).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Field Mappings", () => {
|
||||
const testNodeTypes: NodeType[] = [
|
||||
"dataInput",
|
||||
"llmTask",
|
||||
"webScraping",
|
||||
"databaseQuery",
|
||||
];
|
||||
|
||||
test("should return fields for each node type", () => {
|
||||
testNodeTypes.forEach((nodeType) => {
|
||||
const fields = getFieldsForNodeType(nodeType);
|
||||
expect(fields).toBeInstanceOf(Array);
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("should have configurations for all mapped fields", () => {
|
||||
testNodeTypes.forEach((nodeType) => {
|
||||
const fields = getFieldsForNodeType(nodeType);
|
||||
fields.forEach((fieldName) => {
|
||||
const fieldConfig = getFieldConfig(fieldName);
|
||||
expect(fieldConfig).toBeDefined();
|
||||
expect(fieldConfig.userFriendlyName).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content Quality", () => {
|
||||
test("should have non-technical language in user-friendly names", () => {
|
||||
Object.entries(nodeTypeConfigs).forEach(([nodeType, config]) => {
|
||||
// Check that user-friendly names don't contain technical jargon
|
||||
const technicalTerms = [
|
||||
"API",
|
||||
"JSON",
|
||||
"HTTP",
|
||||
"SQL",
|
||||
"LLM",
|
||||
"URL",
|
||||
"CSV",
|
||||
];
|
||||
technicalTerms.forEach((term) => {
|
||||
expect(config.userFriendlyName).not.toContain(term);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should have clear, actionable help text", () => {
|
||||
Object.entries(nodeTypeConfigs).forEach(([nodeType, config]) => {
|
||||
expect(config.helpText).toMatch(/[.!?]$/); // Should end with punctuation
|
||||
expect(config.helpText).not.toContain("undefined");
|
||||
expect(config.helpText).not.toContain("null");
|
||||
});
|
||||
});
|
||||
|
||||
test("should have practical common use cases", () => {
|
||||
Object.entries(nodeTypeConfigs).forEach(([nodeType, config]) => {
|
||||
config.commonUseCases.forEach((useCase) => {
|
||||
expect(useCase).toBeTruthy();
|
||||
expect(useCase.length).toBeGreaterThan(10);
|
||||
expect(useCase).toMatch(/^[A-Z]/); // Should start with capital letter
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useState } from "react";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { getFieldConfig } from "../config/fieldConfigs";
|
||||
import { Input } from "./ui/input";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./ui/select";
|
||||
|
||||
interface FieldWithHelpProps {
|
||||
fieldName: string;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
type: string;
|
||||
options?: string[];
|
||||
multiple?: boolean;
|
||||
placeholder?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FieldWithHelp: React.FC<FieldWithHelpProps> = ({
|
||||
fieldName,
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
options = [],
|
||||
multiple = false,
|
||||
placeholder,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
className = "",
|
||||
}) => {
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const fieldConfig = getFieldConfig(fieldName);
|
||||
|
||||
if (!fieldConfig) {
|
||||
// Fallback for unknown fields
|
||||
return (
|
||||
<div className={`field-group ${className}`}>
|
||||
<label className="field-label">
|
||||
{fieldName}
|
||||
<button
|
||||
onClick={() => setShowHelp(!showHelp)}
|
||||
className="help-icon ml-1"
|
||||
title="Get help with this field"
|
||||
aria-label={`Get help for ${fieldName}`}
|
||||
>
|
||||
<HelpCircle className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
type={type}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="field-input"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderInput = () => {
|
||||
switch (type) {
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="field-input"
|
||||
placeholder={placeholder || fieldConfig.examples[0]}
|
||||
rows={4}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
if (multiple) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{options.map((option) => (
|
||||
<label key={option} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Array.isArray(value) && value.includes(option)}
|
||||
onChange={(e) => {
|
||||
const currentValues = Array.isArray(value) ? value : [];
|
||||
if (e.target.checked) {
|
||||
onChange([...currentValues, option]);
|
||||
} else {
|
||||
onChange(
|
||||
currentValues.filter((v: string) => v !== option)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm">{option}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select value={value || ""} onValueChange={onChange}>
|
||||
<SelectTrigger className="field-input">
|
||||
<SelectValue
|
||||
placeholder={placeholder || fieldConfig.examples[0]}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
case "checkbox":
|
||||
return (
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value || false}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm">{fieldConfig.userFriendlyName}</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
case "number":
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
||||
className="field-input"
|
||||
placeholder={placeholder || fieldConfig.examples[0]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
type={type}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="field-input"
|
||||
placeholder={placeholder || fieldConfig.examples[0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`field-group ${className}`}>
|
||||
<label className="field-label">
|
||||
<span className="user-friendly-name">
|
||||
{fieldConfig.userFriendlyName}
|
||||
</span>
|
||||
<span className="technical-name">({fieldConfig.technicalName})</span>
|
||||
<button
|
||||
onClick={() => setShowHelp(!showHelp)}
|
||||
className="help-icon ml-1"
|
||||
title="Get help with this field"
|
||||
aria-label={`Get help for ${fieldConfig.userFriendlyName}`}
|
||||
>
|
||||
<HelpCircle className="w-4 h-4 text-gray-400 hover:text-blue-500" />
|
||||
</button>
|
||||
</label>
|
||||
|
||||
{showHelp && (
|
||||
<div className="help-panel mb-2 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="text-sm text-gray-700 mb-2">
|
||||
{fieldConfig.helpText}
|
||||
</div>
|
||||
{fieldConfig.examples.length > 0 && (
|
||||
<div className="text-xs text-gray-600">
|
||||
<strong>Examples:</strong>
|
||||
<ul className="list-disc list-inside mt-1">
|
||||
{fieldConfig.examples.map((example, index) => (
|
||||
<li key={index}>{example}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderInput()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -19,6 +19,9 @@ import { Badge } from "./ui/badge";
|
||||
import { DatabaseNodeConfiguration } from "./DatabaseNodeConfiguration";
|
||||
import { LabelChangeDialog } from "./LabelChangeDialog";
|
||||
import { LabelDependencyManager } from "../utils/labelDependencyManager";
|
||||
import { FieldWithHelp } from "./FieldWithHelp";
|
||||
import { getNodeTypeConfig } from "../config/nodeTypeConfigs";
|
||||
import { getFieldsForNodeType } from "../config/fieldConfigs";
|
||||
|
||||
interface NodeConfigurationProps {
|
||||
nodeId: string;
|
||||
@@ -531,119 +534,35 @@ Return only the optimized prompt:`,
|
||||
const renderField = (field: any) => {
|
||||
const value = currentData.config[field.key] || field.defaultValue || "";
|
||||
|
||||
switch (field.type) {
|
||||
case "textarea":
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => handleConfigChange(field.key, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
rows={3}
|
||||
/>
|
||||
{data.type === "llmTask" && field.key === "prompt" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
onClick={handleOptimizePrompt}
|
||||
disabled={isOptimizing || !currentData.config.prompt}
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
{isOptimizing ? "Optimizing..." : "Optimize Prompt"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<FieldWithHelp
|
||||
fieldName={field.key}
|
||||
value={value}
|
||||
onChange={(newValue) => handleConfigChange(field.key, newValue)}
|
||||
type={field.type}
|
||||
options={field.options}
|
||||
multiple={field.multiple}
|
||||
placeholder={field.placeholder}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
/>
|
||||
{data.type === "llmTask" && field.key === "prompt" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
onClick={handleOptimizePrompt}
|
||||
disabled={isOptimizing || !currentData.config.prompt}
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
{isOptimizing ? "Optimizing..." : "Optimize Prompt"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
case "select":
|
||||
if (field.multiple) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{field.options.map((option: string) => (
|
||||
<label key={option} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
Array.isArray(value) ? value.includes(option) : false
|
||||
}
|
||||
onChange={(e) => {
|
||||
const currentValues = Array.isArray(value) ? value : [];
|
||||
if (e.target.checked) {
|
||||
handleConfigChange(field.key, [
|
||||
...currentValues,
|
||||
option,
|
||||
]);
|
||||
} else {
|
||||
handleConfigChange(
|
||||
field.key,
|
||||
currentValues.filter((v: string) => v !== option)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{option}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(newValue: string) =>
|
||||
handleConfigChange(field.key, newValue)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`Select ${field.label}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options.map((option: string) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
case "checkbox":
|
||||
return (
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={(e) => handleConfigChange(field.key, e.target.checked)}
|
||||
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Enable {field.label}</span>
|
||||
</label>
|
||||
);
|
||||
case "number":
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
handleConfigChange(field.key, parseFloat(e.target.value) || 0)
|
||||
}
|
||||
placeholder={field.placeholder}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => handleConfigChange(field.key, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -652,9 +571,15 @@ Return only the optimized prompt:`,
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Settings className="w-5 h-5 text-gray-600" />
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">
|
||||
{config.title}
|
||||
</CardTitle>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">
|
||||
{getNodeTypeConfig(data.type).userFriendlyName} Configuration
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-500">
|
||||
{getNodeTypeConfig(data.type).technicalName} •{" "}
|
||||
{getNodeTypeConfig(data.type).description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
@@ -667,24 +592,16 @@ Return only the optimized prompt:`,
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4 overflow-y-auto max-h-[60vh]">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Node Label
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={currentData.label}
|
||||
onChange={(e) => handleLabelChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<FieldWithHelp
|
||||
fieldName="label"
|
||||
value={currentData.label}
|
||||
onChange={handleLabelChange}
|
||||
type="text"
|
||||
placeholder="Enter node label"
|
||||
/>
|
||||
|
||||
{config.fields.map((field) => (
|
||||
<div key={field.key}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{field.label}
|
||||
</label>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
<div key={field.key}>{renderField(field)}</div>
|
||||
))}
|
||||
|
||||
{/* Optimization Result Display */}
|
||||
|
||||
+164
-164
@@ -14,7 +14,13 @@ import {
|
||||
// ChevronRight, // Removed unused import
|
||||
Zap,
|
||||
Search as SearchIcon,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getNodeTypeConfig,
|
||||
getNodeTypesByCategory,
|
||||
} from "../config/nodeTypeConfigs";
|
||||
import { NodeType } from "../types";
|
||||
|
||||
export const NodeLibrary: React.FC = () => {
|
||||
const { panelStates, togglePanel } = useWorkflowStore();
|
||||
@@ -24,6 +30,7 @@ export const NodeLibrary: React.FC = () => {
|
||||
const [focusedNodeIndex, setFocusedNodeIndex] = useState(-1);
|
||||
const [focusedCategoryIndex, setFocusedCategoryIndex] = useState(0);
|
||||
const [announcement, setAnnouncement] = useState("");
|
||||
const [showHelp, setShowHelp] = useState<string | null>(null);
|
||||
|
||||
// Refs for focus management
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -34,128 +41,69 @@ export const NodeLibrary: React.FC = () => {
|
||||
const isCollapsed = panelStates.nodeLibrary.isCollapsed;
|
||||
const isHidden = panelStates.nodeLibrary.isHidden;
|
||||
|
||||
const nodeTypesList = [
|
||||
{
|
||||
type: "dataInput",
|
||||
label: "Data Input",
|
||||
icon: ArrowRight,
|
||||
description: "Start your workflow with data",
|
||||
color: "blue",
|
||||
category: "Input",
|
||||
},
|
||||
{
|
||||
type: "webScraping",
|
||||
label: "Web Scraping",
|
||||
icon: Globe,
|
||||
description: "Extract content from websites",
|
||||
color: "green",
|
||||
category: "Data",
|
||||
},
|
||||
{
|
||||
type: "llmTask",
|
||||
label: "AI Task",
|
||||
icon: Brain,
|
||||
description: "Process with AI models",
|
||||
color: "purple",
|
||||
category: "AI",
|
||||
},
|
||||
{
|
||||
type: "embeddingGenerator",
|
||||
label: "Embedding",
|
||||
icon: Zap,
|
||||
description: "Generate vector embeddings",
|
||||
color: "yellow",
|
||||
category: "AI",
|
||||
},
|
||||
{
|
||||
type: "similaritySearch",
|
||||
label: "Similarity Search",
|
||||
icon: SearchIcon,
|
||||
description: "Find similar content",
|
||||
color: "orange",
|
||||
category: "AI",
|
||||
},
|
||||
{
|
||||
type: "structuredOutput",
|
||||
label: "Structured Output",
|
||||
icon: FileText,
|
||||
description: "Format data with schemas",
|
||||
color: "indigo",
|
||||
category: "Output",
|
||||
},
|
||||
{
|
||||
type: "dataOutput",
|
||||
label: "Data Output",
|
||||
icon: ArrowRight,
|
||||
description: "Export your results",
|
||||
color: "gray",
|
||||
category: "Output",
|
||||
},
|
||||
{
|
||||
type: "databaseQuery",
|
||||
label: "Database Query",
|
||||
icon: Database,
|
||||
description: "Query database with SQL",
|
||||
color: "cyan",
|
||||
category: "Database",
|
||||
},
|
||||
{
|
||||
type: "databaseInsert",
|
||||
label: "Database Insert",
|
||||
icon: Database,
|
||||
description: "Insert data into database",
|
||||
color: "cyan",
|
||||
category: "Database",
|
||||
},
|
||||
{
|
||||
type: "databaseUpdate",
|
||||
label: "Database Update",
|
||||
icon: Database,
|
||||
description: "Update database records",
|
||||
color: "cyan",
|
||||
category: "Database",
|
||||
},
|
||||
{
|
||||
type: "databaseDelete",
|
||||
label: "Database Delete",
|
||||
icon: Database,
|
||||
description: "Delete database records",
|
||||
color: "cyan",
|
||||
category: "Database",
|
||||
},
|
||||
const nodeTypesList: NodeType[] = [
|
||||
"dataInput",
|
||||
"webScraping",
|
||||
"llmTask",
|
||||
"embeddingGenerator",
|
||||
"similaritySearch",
|
||||
"structuredOutput",
|
||||
"dataOutput",
|
||||
"databaseQuery",
|
||||
"databaseInsert",
|
||||
"databaseUpdate",
|
||||
"databaseDelete",
|
||||
];
|
||||
|
||||
const nodeTypeIcons: Record<NodeType, any> = {
|
||||
dataInput: ArrowRight,
|
||||
webScraping: Globe,
|
||||
llmTask: Brain,
|
||||
embeddingGenerator: Zap,
|
||||
similaritySearch: SearchIcon,
|
||||
structuredOutput: FileText,
|
||||
dataOutput: ArrowRight,
|
||||
databaseQuery: Database,
|
||||
databaseInsert: Database,
|
||||
databaseUpdate: Database,
|
||||
databaseDelete: Database,
|
||||
};
|
||||
|
||||
const nodeTypeColors: Record<NodeType, string> = {
|
||||
dataInput: "blue",
|
||||
webScraping: "green",
|
||||
llmTask: "purple",
|
||||
embeddingGenerator: "yellow",
|
||||
similaritySearch: "orange",
|
||||
structuredOutput: "indigo",
|
||||
dataOutput: "gray",
|
||||
databaseQuery: "cyan",
|
||||
databaseInsert: "cyan",
|
||||
databaseUpdate: "cyan",
|
||||
databaseDelete: "cyan",
|
||||
};
|
||||
|
||||
const nodeTypesByCategory = getNodeTypesByCategory();
|
||||
const categories = [
|
||||
{ name: "All", count: nodeTypesList.length },
|
||||
{
|
||||
name: "Input",
|
||||
count: nodeTypesList.filter((n) => n.category === "Input").length,
|
||||
},
|
||||
{
|
||||
name: "AI",
|
||||
count: nodeTypesList.filter((n) => n.category === "AI").length,
|
||||
},
|
||||
{
|
||||
name: "Data",
|
||||
count: nodeTypesList.filter((n) => n.category === "Data").length,
|
||||
},
|
||||
{
|
||||
name: "Database",
|
||||
count: nodeTypesList.filter((n) => n.category === "Database").length,
|
||||
},
|
||||
{
|
||||
name: "Output",
|
||||
count: nodeTypesList.filter((n) => n.category === "Output").length,
|
||||
},
|
||||
...Object.entries(nodeTypesByCategory).map(([category, nodes]) => ({
|
||||
name: category,
|
||||
count: nodes.length,
|
||||
})),
|
||||
];
|
||||
|
||||
const filteredNodes = nodeTypesList.filter(
|
||||
(nodeType) =>
|
||||
(selectedCategory === "All" || nodeType.category === selectedCategory) &&
|
||||
const filteredNodes = nodeTypesList.filter((nodeType) => {
|
||||
const config = getNodeTypeConfig(nodeType);
|
||||
return (
|
||||
(selectedCategory === "All" || config.category === selectedCategory) &&
|
||||
(searchQuery === "" ||
|
||||
nodeType.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
nodeType.description.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
config.userFriendlyName
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase()) ||
|
||||
config.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
config.technicalName.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
});
|
||||
|
||||
// Accessibility functions
|
||||
const announceToScreenReader = useCallback((message: string) => {
|
||||
@@ -173,7 +121,10 @@ export const NodeLibrary: React.FC = () => {
|
||||
if (focusedNodeIndex < filteredNodes.length - 1) {
|
||||
setFocusedNodeIndex(focusedNodeIndex + 1);
|
||||
announceToScreenReader(
|
||||
`Focused on ${filteredNodes[focusedNodeIndex + 1]?.label}`
|
||||
`Focused on ${
|
||||
getNodeTypeConfig(filteredNodes[focusedNodeIndex + 1])
|
||||
.userFriendlyName
|
||||
}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -182,7 +133,10 @@ export const NodeLibrary: React.FC = () => {
|
||||
if (focusedNodeIndex > 0) {
|
||||
setFocusedNodeIndex(focusedNodeIndex - 1);
|
||||
announceToScreenReader(
|
||||
`Focused on ${filteredNodes[focusedNodeIndex - 1]?.label}`
|
||||
`Focused on ${
|
||||
getNodeTypeConfig(filteredNodes[focusedNodeIndex - 1])
|
||||
.userFriendlyName
|
||||
}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -196,7 +150,9 @@ export const NodeLibrary: React.FC = () => {
|
||||
const nodeType = filteredNodes[focusedNodeIndex];
|
||||
// Simulate drag and drop for keyboard users
|
||||
announceToScreenReader(
|
||||
`Adding ${nodeType.label} to canvas. Use mouse to position the node.`
|
||||
`Adding ${
|
||||
getNodeTypeConfig(nodeType).userFriendlyName
|
||||
} to canvas. Use mouse to position the node.`
|
||||
);
|
||||
// In a real implementation, you would trigger the node creation here
|
||||
}
|
||||
@@ -510,59 +466,103 @@ export const NodeLibrary: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
filteredNodes.map((nodeType, index) => {
|
||||
const IconComponent = nodeType.icon;
|
||||
const config = getNodeTypeConfig(nodeType);
|
||||
const IconComponent = nodeTypeIcons[nodeType];
|
||||
const isFocused = focusedNodeIndex === index;
|
||||
const isHelpOpen = showHelp === nodeType;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={nodeType.type}
|
||||
data-node-index={index}
|
||||
className={`group flex items-center space-x-3 p-3 rounded-xl border cursor-move transition-all duration-200 hover:shadow-md transform hover:-translate-y-0.5 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none ${
|
||||
isFocused ? "ring-2 ring-blue-500 ring-offset-2" : ""
|
||||
} ${getColorClasses(nodeType.color)}`}
|
||||
draggable
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, nodeType.type)
|
||||
}
|
||||
onClick={() => handleNodeClick(nodeType)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleNodeClick(nodeType);
|
||||
<div key={nodeType} className="space-y-2">
|
||||
<div
|
||||
data-node-index={index}
|
||||
className={`group flex items-center space-x-3 p-3 rounded-xl border cursor-move transition-all duration-200 hover:shadow-md transform hover:-translate-y-0.5 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none ${
|
||||
isFocused
|
||||
? "ring-2 ring-blue-500 ring-offset-2"
|
||||
: ""
|
||||
} ${getColorClasses(nodeTypeColors[nodeType])}`}
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, nodeType)}
|
||||
onClick={() =>
|
||||
handleNodeClick({
|
||||
type: nodeType,
|
||||
label: config.userFriendlyName,
|
||||
})
|
||||
}
|
||||
}}
|
||||
role="option"
|
||||
aria-selected={isFocused}
|
||||
tabIndex={isFocused ? 0 : -1}
|
||||
aria-label={`${nodeType.label} node, ${nodeType.category} category. ${nodeType.description}. Drag to canvas or press Enter to add.`}
|
||||
aria-describedby={`node-${nodeType.type}-description`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg bg-white/80 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<IconComponent className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{nodeType.label}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 bg-white/60 rounded-full"
|
||||
aria-label={`Category: ${nodeType.category}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleNodeClick({
|
||||
type: nodeType,
|
||||
label: config.userFriendlyName,
|
||||
});
|
||||
}
|
||||
}}
|
||||
role="option"
|
||||
aria-selected={isFocused}
|
||||
tabIndex={isFocused ? 0 : -1}
|
||||
aria-label={`${config.userFriendlyName} node, ${config.category} category. ${config.description}. Drag to canvas or press Enter to add.`}
|
||||
aria-describedby={`node-${nodeType}-description`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg bg-white/80 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{nodeType.category}
|
||||
</span>
|
||||
<span className="text-lg">{config.icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
id={`node-${nodeType.type}-description`}
|
||||
className="text-xs text-gray-500 truncate"
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{config.userFriendlyName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({config.technicalName})
|
||||
</span>
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 bg-white/60 rounded-full"
|
||||
aria-label={`Category: ${config.category}`}
|
||||
>
|
||||
{config.category}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
id={`node-${nodeType}-description`}
|
||||
className="text-xs text-gray-500 truncate"
|
||||
>
|
||||
{config.description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowHelp(isHelpOpen ? null : nodeType);
|
||||
}}
|
||||
className="help-icon opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Get help with this node"
|
||||
aria-label={`Get help for ${config.userFriendlyName}`}
|
||||
>
|
||||
{nodeType.description}
|
||||
</p>
|
||||
<HelpCircle className="w-4 h-4 text-gray-400 hover:text-blue-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isHelpOpen && (
|
||||
<div className="help-panel p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div className="text-sm text-gray-700 mb-2">
|
||||
{config.helpText}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
<strong>Common uses:</strong>
|
||||
<ul className="list-disc list-inside mt-1 space-y-1">
|
||||
{config.commonUseCases.map(
|
||||
(useCase, useIndex) => (
|
||||
<li key={useIndex}>{useCase}</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
export interface FieldConfig {
|
||||
technicalName: string;
|
||||
userFriendlyName: string;
|
||||
description: string;
|
||||
helpText: string;
|
||||
examples: string[];
|
||||
validation?: {
|
||||
required: boolean;
|
||||
pattern?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const fieldConfigs: Record<string, FieldConfig> = {
|
||||
// Data Input Fields
|
||||
dataType: {
|
||||
technicalName: "dataType",
|
||||
userFriendlyName: "What type of data?",
|
||||
description: "Choose the format of your input data",
|
||||
helpText:
|
||||
"Select the type of data you're working with. This helps the system process your data correctly.",
|
||||
examples: [
|
||||
"Text document",
|
||||
"CSV spreadsheet",
|
||||
"Website URL",
|
||||
"PDF file",
|
||||
"JSON data",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
defaultValue: {
|
||||
technicalName: "defaultValue",
|
||||
userFriendlyName: "Sample data",
|
||||
description: "Provide example data to test your workflow",
|
||||
helpText:
|
||||
"Enter sample data that represents what you'll be processing. This helps test your workflow before using real data.",
|
||||
examples: [
|
||||
"John Doe, john@example.com, 555-1234",
|
||||
"https://example.com",
|
||||
"Sample product description",
|
||||
],
|
||||
},
|
||||
|
||||
// AI Task Fields
|
||||
prompt: {
|
||||
technicalName: "prompt",
|
||||
userFriendlyName: "What should AI do?",
|
||||
description: "Tell the AI what task to perform on your data",
|
||||
helpText:
|
||||
"Write clear instructions for what you want the AI to do. Be specific about the output format you want.",
|
||||
examples: [
|
||||
"Summarize this text in 3 bullet points",
|
||||
"Extract the main topics from this document",
|
||||
"Translate this text to Spanish",
|
||||
"Generate a product description based on these features",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
minLength: 10,
|
||||
},
|
||||
},
|
||||
model: {
|
||||
technicalName: "model",
|
||||
userFriendlyName: "AI Assistant type",
|
||||
description: "Choose which AI model to use for your task",
|
||||
helpText:
|
||||
"Different AI models are better at different tasks. The default option works well for most cases.",
|
||||
examples: ["DeepSeek Chat (recommended)", "OpenAI GPT-4", "OpenAI GPT-3.5"],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
temperature: {
|
||||
technicalName: "temperature",
|
||||
userFriendlyName: "How creative should AI be?",
|
||||
description: "Control how creative or focused the AI responses are",
|
||||
helpText:
|
||||
"Lower values (0.1-0.3) give more focused, consistent responses. Higher values (0.7-1.0) give more creative, varied responses.",
|
||||
examples: [
|
||||
"0.1 - Very focused (good for facts)",
|
||||
"0.7 - Balanced (good for most tasks)",
|
||||
"1.0 - Very creative (good for writing)",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
maxTokens: {
|
||||
technicalName: "maxTokens",
|
||||
userFriendlyName: "Response length",
|
||||
description: "Set the maximum length of AI responses",
|
||||
helpText:
|
||||
"Controls how long the AI response can be. Shorter responses are faster and cheaper, longer responses give more detail.",
|
||||
examples: [
|
||||
"100 - Very short (1-2 sentences)",
|
||||
"500 - Short (1 paragraph)",
|
||||
"1000 - Medium (2-3 paragraphs)",
|
||||
"2000 - Long (detailed response)",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Web Scraping Fields
|
||||
url: {
|
||||
technicalName: "url",
|
||||
userFriendlyName: "Website address",
|
||||
description: "Enter the URL of the website to scrape",
|
||||
helpText:
|
||||
"Provide the full website address (including https://) that you want to extract content from.",
|
||||
examples: [
|
||||
"https://example.com",
|
||||
"https://news.ycombinator.com",
|
||||
"https://github.com/trending",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
pattern: "^https?://.+",
|
||||
},
|
||||
},
|
||||
formats: {
|
||||
technicalName: "formats",
|
||||
userFriendlyName: "What format do you want?",
|
||||
description: "Choose how you want the website content formatted",
|
||||
helpText:
|
||||
"Select the format that best fits your needs. Markdown is great for text, HTML preserves formatting, and JSON is good for structured data.",
|
||||
examples: [
|
||||
"Markdown - Clean text format",
|
||||
"HTML - Preserves website formatting",
|
||||
"JSON - Structured data format",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
maxLength: {
|
||||
technicalName: "maxLength",
|
||||
userFriendlyName: "Maximum content length",
|
||||
description: "Set the maximum amount of content to extract",
|
||||
helpText:
|
||||
"Limit how much content to extract from the website. This helps avoid processing very long pages and keeps costs down.",
|
||||
examples: [
|
||||
"1000 - Short article",
|
||||
"5000 - Medium article",
|
||||
"10000 - Long article",
|
||||
"20000 - Very long content",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
onlyMainContent: {
|
||||
technicalName: "onlyMainContent",
|
||||
userFriendlyName: "Extract main content only?",
|
||||
description: "Focus on the main article content, skip navigation and ads",
|
||||
helpText:
|
||||
"When enabled, this extracts only the main content of the page, filtering out navigation menus, advertisements, and other non-essential elements.",
|
||||
examples: [
|
||||
"Yes - Clean article content only",
|
||||
"No - Include all page elements",
|
||||
],
|
||||
},
|
||||
includeTags: {
|
||||
technicalName: "includeTags",
|
||||
userFriendlyName: "Include specific elements",
|
||||
description: "Specify which HTML elements to include",
|
||||
helpText:
|
||||
"Enter CSS selectors to include specific parts of the page. Leave empty to include everything.",
|
||||
examples: ["h1, h2, p", ".article-content", "#main"],
|
||||
},
|
||||
excludeTags: {
|
||||
technicalName: "excludeTags",
|
||||
userFriendlyName: "Exclude specific elements",
|
||||
description: "Specify which HTML elements to exclude",
|
||||
helpText:
|
||||
"Enter CSS selectors to exclude specific parts of the page like ads, navigation, or footers.",
|
||||
examples: [".advertisement", "nav", ".sidebar", "footer"],
|
||||
},
|
||||
|
||||
// Database Fields
|
||||
query: {
|
||||
technicalName: "query",
|
||||
userFriendlyName: "What data do you want?",
|
||||
description: "Write a query to get specific data from your database",
|
||||
helpText:
|
||||
"Write a database query to retrieve the data you need. Use simple language or SQL depending on your database type.",
|
||||
examples: [
|
||||
"SELECT * FROM customers WHERE created_at > '2024-01-01'",
|
||||
"Find all products with price > 100",
|
||||
"Get the last 10 orders",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
minLength: 5,
|
||||
},
|
||||
},
|
||||
table: {
|
||||
technicalName: "table",
|
||||
userFriendlyName: "Which table?",
|
||||
description: "Select the database table to work with",
|
||||
helpText:
|
||||
"Choose which table in your database contains the data you want to work with.",
|
||||
examples: ["customers", "products", "orders", "inventory"],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
connectionId: {
|
||||
technicalName: "connectionId",
|
||||
userFriendlyName: "Database connection",
|
||||
description: "Choose which database to connect to",
|
||||
helpText:
|
||||
"Select a pre-configured database connection. You can manage connections in the Database settings.",
|
||||
examples: ["Production Database", "Test Database", "Analytics Database"],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Output Fields
|
||||
format: {
|
||||
technicalName: "format",
|
||||
userFriendlyName: "Output format",
|
||||
description: "Choose how to format your final results",
|
||||
helpText:
|
||||
"Select the format for your output data. This determines how your results will be saved or displayed.",
|
||||
examples: [
|
||||
"JSON - Structured data",
|
||||
"CSV - Spreadsheet format",
|
||||
"TXT - Plain text",
|
||||
"HTML - Web format",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
filename: {
|
||||
technicalName: "filename",
|
||||
userFriendlyName: "File name",
|
||||
description: "Name for the output file",
|
||||
helpText:
|
||||
"Enter a name for the file where your results will be saved. Include the file extension (.csv, .txt, .json).",
|
||||
examples: ["report.csv", "analysis.txt", "data.json", "results.html"],
|
||||
validation: {
|
||||
required: true,
|
||||
pattern: '^[^<>:"/\\\\|?*]+\\.[a-zA-Z0-9]+$',
|
||||
},
|
||||
},
|
||||
|
||||
// Similarity Search Fields
|
||||
vectorStore: {
|
||||
technicalName: "vectorStore",
|
||||
userFriendlyName: "Search database",
|
||||
description: "Choose where to search for similar content",
|
||||
helpText:
|
||||
"Select the vector database where your content is indexed. This is where the AI will search for similar content.",
|
||||
examples: ["Pinecone", "Weaviate", "Chroma"],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
topK: {
|
||||
technicalName: "topK",
|
||||
userFriendlyName: "How many results?",
|
||||
description: "Number of similar results to return",
|
||||
helpText:
|
||||
"Set how many similar results you want to find. More results give you more options but may include less relevant matches.",
|
||||
examples: [
|
||||
"3 - Few results (most relevant)",
|
||||
"5 - Moderate results",
|
||||
"10 - Many results",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
threshold: {
|
||||
technicalName: "threshold",
|
||||
userFriendlyName: "How similar should results be?",
|
||||
description: "Minimum similarity score for results",
|
||||
helpText:
|
||||
"Set how similar results need to be to your search. Higher values (0.8-1.0) return only very similar content. Lower values (0.5-0.7) return more varied results.",
|
||||
examples: [
|
||||
"0.9 - Very similar only",
|
||||
"0.8 - Similar",
|
||||
"0.6 - Somewhat similar",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Structured Output Fields
|
||||
schema: {
|
||||
technicalName: "schema",
|
||||
userFriendlyName: "Data structure",
|
||||
description: "Define how you want your data organized",
|
||||
helpText:
|
||||
"Describe the structure you want for your output data. This helps the AI format your results consistently.",
|
||||
examples: [
|
||||
"Name, Email, Phone",
|
||||
"Title, Description, Price, Category",
|
||||
"Date, Amount, Description, Category",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
minLength: 5,
|
||||
},
|
||||
},
|
||||
|
||||
// General Fields
|
||||
label: {
|
||||
technicalName: "label",
|
||||
userFriendlyName: "Node name",
|
||||
description: "Give this node a descriptive name",
|
||||
helpText:
|
||||
"Choose a name that describes what this node does. This helps you identify it in your workflow and use it in other nodes.",
|
||||
examples: [
|
||||
"Customer Data Input",
|
||||
"AI Content Analyzer",
|
||||
"Email Report Generator",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
minLength: 3,
|
||||
maxLength: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getFieldConfig = (fieldName: string): FieldConfig | null => {
|
||||
return fieldConfigs[fieldName] || null;
|
||||
};
|
||||
|
||||
export const getFieldsForNodeType = (nodeType: string): string[] => {
|
||||
const fieldMappings: Record<string, string[]> = {
|
||||
dataInput: ["dataType", "defaultValue", "label"],
|
||||
webScraping: [
|
||||
"url",
|
||||
"formats",
|
||||
"maxLength",
|
||||
"onlyMainContent",
|
||||
"includeTags",
|
||||
"excludeTags",
|
||||
"label",
|
||||
],
|
||||
llmTask: ["prompt", "model", "temperature", "maxTokens", "label"],
|
||||
embeddingGenerator: ["model", "label"],
|
||||
similaritySearch: ["vectorStore", "topK", "threshold", "label"],
|
||||
structuredOutput: ["schema", "model", "label"],
|
||||
dataOutput: ["format", "filename", "label"],
|
||||
databaseQuery: ["connectionId", "query", "table", "label"],
|
||||
databaseInsert: ["connectionId", "table", "label"],
|
||||
databaseUpdate: ["connectionId", "table", "label"],
|
||||
databaseDelete: ["connectionId", "table", "label"],
|
||||
};
|
||||
|
||||
return fieldMappings[nodeType] || [];
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
import { NodeType } from "../types";
|
||||
|
||||
export interface NodeTypeConfig {
|
||||
technicalName: string;
|
||||
userFriendlyName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
helpText: string;
|
||||
commonUseCases: string[];
|
||||
}
|
||||
|
||||
export const nodeTypeConfigs: Record<NodeType, NodeTypeConfig> = {
|
||||
dataInput: {
|
||||
technicalName: "dataInput",
|
||||
userFriendlyName: "Get Data From",
|
||||
description: "Import data from files, websites, or other sources",
|
||||
category: "Data Sources",
|
||||
icon: "📥",
|
||||
helpText:
|
||||
"This node is your starting point. It brings data into your workflow from files, websites, databases, or manual input.",
|
||||
commonUseCases: [
|
||||
"Upload a CSV file with customer data",
|
||||
"Enter a website URL to scrape content",
|
||||
"Connect to your database to get records",
|
||||
],
|
||||
},
|
||||
webScraping: {
|
||||
technicalName: "webScraping",
|
||||
userFriendlyName: "Get Website Content",
|
||||
description: "Extract text, images, and data from websites",
|
||||
category: "Data Sources",
|
||||
icon: "🌐",
|
||||
helpText:
|
||||
"Automatically visit websites and extract the content you need. Perfect for gathering information from news sites, product pages, or any public webpage.",
|
||||
commonUseCases: [
|
||||
"Scrape product prices from e-commerce sites",
|
||||
"Extract news articles for analysis",
|
||||
"Get contact information from business directories",
|
||||
],
|
||||
},
|
||||
llmTask: {
|
||||
technicalName: "llmTask",
|
||||
userFriendlyName: "AI Assistant",
|
||||
description: "Use AI to analyze, summarize, or transform your data",
|
||||
category: "Processing",
|
||||
icon: "🤖",
|
||||
helpText:
|
||||
"Your AI-powered helper that can read, understand, and work with text data. It can summarize documents, answer questions, translate languages, or generate new content.",
|
||||
commonUseCases: [
|
||||
"Summarize long documents",
|
||||
"Generate product descriptions",
|
||||
"Translate text to different languages",
|
||||
"Extract key information from emails",
|
||||
],
|
||||
},
|
||||
embeddingGenerator: {
|
||||
technicalName: "embeddingGenerator",
|
||||
userFriendlyName: "Create Search Index",
|
||||
description:
|
||||
"Convert text into searchable format for finding similar content",
|
||||
category: "Processing",
|
||||
icon: "🧠",
|
||||
helpText:
|
||||
"Transforms your text into a format that AI can use to find similar content. Think of it as creating a smart index for your data.",
|
||||
commonUseCases: [
|
||||
"Make your documents searchable",
|
||||
"Find similar articles or posts",
|
||||
"Create a knowledge base from your content",
|
||||
],
|
||||
},
|
||||
similaritySearch: {
|
||||
technicalName: "similaritySearch",
|
||||
userFriendlyName: "Find Similar Content",
|
||||
description:
|
||||
"Search through your data to find content similar to what you're looking for",
|
||||
category: "Processing",
|
||||
icon: "🔍",
|
||||
helpText:
|
||||
"Uses AI to find content that's similar to your search. Great for finding related articles, similar products, or matching customer inquiries.",
|
||||
commonUseCases: [
|
||||
"Find similar customer support tickets",
|
||||
"Match job descriptions to resumes",
|
||||
"Find related articles in your content library",
|
||||
],
|
||||
},
|
||||
structuredOutput: {
|
||||
technicalName: "structuredOutput",
|
||||
userFriendlyName: "Format Data",
|
||||
description: "Organize and structure your data in a specific format",
|
||||
category: "Processing",
|
||||
icon: "📋",
|
||||
helpText:
|
||||
"Takes messy or unstructured data and organizes it into a clean, consistent format. Perfect for preparing data for reports or other systems.",
|
||||
commonUseCases: [
|
||||
"Convert text into structured tables",
|
||||
"Format data for reports",
|
||||
"Prepare data for import into other systems",
|
||||
],
|
||||
},
|
||||
dataOutput: {
|
||||
technicalName: "dataOutput",
|
||||
userFriendlyName: "Send Data To",
|
||||
description:
|
||||
"Export or send your processed data to files, emails, or other systems",
|
||||
category: "Output",
|
||||
icon: "📤",
|
||||
helpText:
|
||||
"This is where your workflow delivers results. You can save data to files, send emails, update databases, or display results.",
|
||||
commonUseCases: [
|
||||
"Save results to a CSV file",
|
||||
"Send email reports",
|
||||
"Update your CRM with new leads",
|
||||
"Display results on a dashboard",
|
||||
],
|
||||
},
|
||||
databaseQuery: {
|
||||
technicalName: "databaseQuery",
|
||||
userFriendlyName: "Get Database Info",
|
||||
description: "Retrieve specific information from your database",
|
||||
category: "Data Sources",
|
||||
icon: "🗄️",
|
||||
helpText:
|
||||
"Searches your database to find specific records or information. You can ask for customers, products, orders, or any data stored in your database.",
|
||||
commonUseCases: [
|
||||
"Find all customers from last month",
|
||||
"Get product inventory levels",
|
||||
"Retrieve order details",
|
||||
"Search for specific records",
|
||||
],
|
||||
},
|
||||
databaseInsert: {
|
||||
technicalName: "databaseInsert",
|
||||
userFriendlyName: "Add to Database",
|
||||
description: "Add new records to your database",
|
||||
category: "Output",
|
||||
icon: "➕",
|
||||
helpText:
|
||||
"Adds new information to your database. Perfect for saving new customers, products, or any new data you've collected.",
|
||||
commonUseCases: [
|
||||
"Add new customer records",
|
||||
"Save form submissions",
|
||||
"Store new product information",
|
||||
"Record new transactions",
|
||||
],
|
||||
},
|
||||
databaseUpdate: {
|
||||
technicalName: "databaseUpdate",
|
||||
userFriendlyName: "Update Database",
|
||||
description: "Modify existing records in your database",
|
||||
category: "Output",
|
||||
icon: "✏️",
|
||||
helpText:
|
||||
"Changes information that's already in your database. Use this to update customer details, product prices, or any existing records.",
|
||||
commonUseCases: [
|
||||
"Update customer contact information",
|
||||
"Change product prices",
|
||||
"Mark orders as completed",
|
||||
"Update employee records",
|
||||
],
|
||||
},
|
||||
databaseDelete: {
|
||||
technicalName: "databaseDelete",
|
||||
userFriendlyName: "Remove from Database",
|
||||
description: "Delete records from your database",
|
||||
category: "Output",
|
||||
icon: "🗑️",
|
||||
helpText:
|
||||
"Removes records from your database. Use carefully - this action cannot be undone. Good for cleaning up old data or removing duplicates.",
|
||||
commonUseCases: [
|
||||
"Remove duplicate records",
|
||||
"Delete old test data",
|
||||
"Clean up expired records",
|
||||
"Remove cancelled orders",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getNodeTypeConfig = (nodeType: NodeType): NodeTypeConfig => {
|
||||
return (
|
||||
nodeTypeConfigs[nodeType] || {
|
||||
technicalName: nodeType,
|
||||
userFriendlyName: nodeType,
|
||||
description: "Unknown node type",
|
||||
category: "Other",
|
||||
icon: "❓",
|
||||
helpText: "This node type is not yet configured with help information.",
|
||||
commonUseCases: [],
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getNodeTypesByCategory = (): Record<string, NodeType[]> => {
|
||||
const categories: Record<string, NodeType[]> = {};
|
||||
|
||||
Object.entries(nodeTypeConfigs).forEach(([nodeType, config]) => {
|
||||
if (!categories[config.category]) {
|
||||
categories[config.category] = [];
|
||||
}
|
||||
categories[config.category].push(nodeType as NodeType);
|
||||
});
|
||||
|
||||
return categories;
|
||||
};
|
||||
@@ -228,4 +228,45 @@
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Help system styling */
|
||||
.help-icon {
|
||||
@apply inline-flex items-center justify-center w-5 h-5 rounded-full border border-gray-300 bg-white hover:bg-blue-50 hover:border-blue-300 transition-colors;
|
||||
}
|
||||
|
||||
.help-panel {
|
||||
@apply border border-blue-200 rounded-lg shadow-sm;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.user-friendly-name {
|
||||
@apply font-medium text-gray-900;
|
||||
}
|
||||
|
||||
.technical-name {
|
||||
@apply text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
@apply block text-sm font-medium text-gray-700 mb-1 flex items-center;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user