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:
Nikhil-Doye
2025-10-21 23:47:27 -04:00
parent f909a3c38a
commit 2701b4c94c
7 changed files with 1201 additions and 295 deletions
+175
View File
@@ -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
});
});
});
});
});
+206
View File
@@ -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>
);
};
+48 -131
View File
@@ -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
View File
@@ -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>
);
})
+363
View File
@@ -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] || [];
};
+204
View File
@@ -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;
};
+41
View File
@@ -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;
}
}