Files
workflow-builder/src/components/NodeConfiguration.tsx
T
Nikhil-Doye 72918c1ad7 Add Discord integration support across components
- Introduced a new 'discord' entry in the ConfigurationWizard for Discord integration setup.
- Updated NodeLibrary to include Discord as a node type with associated color coding.
- Enhanced BaseNode with Discord icon representation.
- Configured nodeTypeConfigs for Discord with detailed descriptions and common use cases.
- Added connection suggestions for Discord nodes to facilitate integration patterns.
- Implemented DiscordService and discordProcessor for handling various Discord operations, including sending messages, managing channels, and user interactions.
- Created DiscordWizard component for configuring Discord operations with dynamic form fields based on selected operation type.
- Added DiscordNode component for visualizing Discord operation details, including status and configuration.
2025-10-23 16:31:39 -04:00

704 lines
19 KiB
TypeScript

import React, { useState } from "react";
import { NodeData } from "../types";
import { useWorkflowStore } from "../store/workflowStore";
import { X, Settings, Sparkles } from "lucide-react";
import { promptOptimizer } from "../services/promptOptimizer";
import { callOpenAI } from "../services/openaiService";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
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";
interface NodeConfigurationProps {
nodeId: string;
data: NodeData;
onClose: () => void;
}
interface FieldConfig {
key: string;
label: string;
type: string;
placeholder?: string;
options?: string[];
multiple?: boolean;
defaultValue?: any;
min?: number;
max?: number;
step?: number;
}
const nodeTypeConfigs: Record<
string,
{ title: string; fields: FieldConfig[] }
> = {
webScraping: {
title: "Web Scraping Configuration (Firecrawl AI)",
fields: [
{
key: "url",
label: "URL",
type: "text",
placeholder: "https://example.com",
},
{
key: "formats",
label: "Output Formats",
type: "select",
options: ["markdown", "html", "text", "summary", "links", "images"],
multiple: true,
},
{
key: "onlyMainContent",
label: "Only Main Content",
type: "checkbox",
defaultValue: true,
},
{
key: "includeTags",
label: "Include Tags (comma-separated)",
type: "text",
placeholder: "article, main, .content",
},
{
key: "excludeTags",
label: "Exclude Tags (comma-separated)",
type: "text",
placeholder: "nav, footer, .ads",
},
{
key: "maxLength",
label: "Max Length",
type: "number",
placeholder: "5000",
},
{
key: "waitFor",
label: "Wait For (ms)",
type: "number",
placeholder: "2000",
},
{
key: "timeout",
label: "Timeout (ms)",
type: "number",
placeholder: "30000",
},
],
},
llmTask: {
title: "LLM Task Configuration",
fields: [
{
key: "prompt",
label: "Prompt",
type: "textarea",
placeholder: "Enter your prompt...",
},
{
key: "model",
label: "Model",
type: "select",
options: ["deepseek-chat", "deepseek-reasoner"],
},
{
key: "temperature",
label: "Temperature",
type: "number",
min: 0,
max: 2,
step: 0.1,
},
{
key: "maxTokens",
label: "Max Tokens",
type: "number",
placeholder: "1000",
},
],
},
embeddingGenerator: {
title: "Embedding Generator Configuration",
fields: [
{
key: "model",
label: "Model",
type: "select",
options: ["text-embedding-ada-002", "text-embedding-3-small"],
},
{
key: "dimensions",
label: "Dimensions",
type: "number",
placeholder: "1536",
},
],
},
similaritySearch: {
title: "Similarity Search Configuration",
fields: [
{
key: "vectorStore",
label: "Vector Store",
type: "select",
options: ["pinecone"],
},
{
key: "indexName",
label: "Index Name",
type: "text",
placeholder: "my-index",
},
{ key: "topK", label: "Top K Results", type: "number", placeholder: "5" },
{
key: "threshold",
label: "Similarity Threshold",
type: "number",
min: 0,
max: 1,
step: 0.1,
},
],
},
structuredOutput: {
title: "Structured Output Configuration",
fields: [
{
key: "schema",
label: "JSON Schema",
type: "textarea",
placeholder: '{"type": "object", "properties": {...}}',
},
{
key: "model",
label: "Model",
type: "select",
options: ["deepseek-chat", "deepseek-reasoner"],
},
],
},
dataInput: {
title: "Data Input Configuration",
fields: [
{
key: "dataType",
label: "Data Type",
type: "select",
options: ["text", "json", "csv", "url", "pdf"],
},
{
key: "defaultValue",
label: "Default Value",
type: "textarea",
placeholder: "Enter default data...",
},
],
},
dataOutput: {
title: "Data Output Configuration",
fields: [
{
key: "format",
label: "Output Format",
type: "select",
options: ["json", "text", "csv"],
},
{
key: "filename",
label: "Filename",
type: "text",
placeholder: "output.json",
},
],
},
database: {
title: "Database Operations Configuration",
fields: [
{
key: "operation",
label: "Operation Type",
type: "select",
options: [
"query",
"insert",
"update",
"delete",
"aggregate",
"transaction",
],
},
{
key: "connectionId",
label: "Database Connection",
type: "text",
placeholder: "connection-id",
},
{
key: "query",
label: "SQL Query (for query operation)",
type: "textarea",
placeholder: "SELECT * FROM users WHERE id = ?",
},
{
key: "table",
label: "Table Name (for insert/update/delete)",
type: "text",
placeholder: "users",
},
{
key: "data",
label: "Data (JSON)",
type: "textarea",
placeholder: '{"name": "John", "email": "john@example.com"}',
},
],
},
slack: {
title: "Slack Integration Configuration",
fields: [
{
key: "botToken",
label: "Bot Token",
type: "text",
placeholder: "xoxb-your-bot-token",
},
{
key: "operation",
label: "Operation Type",
type: "select",
options: ["message", "channel", "user", "file", "reaction", "reminder"],
},
{
key: "channel",
label: "Channel (for message/channel operations)",
type: "text",
placeholder: "#general or @username",
},
{
key: "text",
label: "Message Text",
type: "textarea",
placeholder: "Hello from the workflow!",
},
{
key: "user",
label: "User ID (for user operations)",
type: "text",
placeholder: "U1234567890",
},
],
},
discord: {
title: "Discord Integration Configuration",
fields: [
{
key: "botToken",
label: "Bot Token",
type: "text",
placeholder: "your-bot-token",
},
{
key: "operation",
label: "Operation Type",
type: "select",
options: [
"message",
"channel",
"user",
"role",
"reaction",
"voice",
"webhook",
],
},
{
key: "channelId",
label: "Channel ID (for message/channel operations)",
type: "text",
placeholder: "123456789012345678",
},
{
key: "message",
label: "Message Text",
type: "textarea",
placeholder: "Hello from the workflow!",
},
{
key: "userId",
label: "User ID (for user operations)",
type: "text",
placeholder: "123456789012345678",
},
],
},
};
export const NodeConfiguration: React.FC<NodeConfigurationProps> = ({
nodeId,
data,
onClose,
}) => {
const { updateNode, updateNodeLabelWithDependencies, currentWorkflow } =
useWorkflowStore();
const config = nodeTypeConfigs[data.type];
const [isOptimizing, setIsOptimizing] = useState(false);
const [optimizationResult, setOptimizationResult] = useState<string>("");
// State for label change dialog
const [showLabelDialog, setShowLabelDialog] = useState(false);
const [pendingLabel, setPendingLabel] = useState<string>("");
// Get the latest node data from the store to ensure we have the most up-to-date config
const currentNode = currentWorkflow?.nodes.find((node) => node.id === nodeId);
const currentData = currentNode?.data || data;
const handleConfigChange = (key: string, value: any) => {
updateNode(nodeId, {
config: {
...currentData.config,
[key]: value,
},
});
};
// Check if this is a database node and use the enhanced configuration
const isDatabaseNode = data.type === "database";
if (isDatabaseNode) {
return (
<DatabaseNodeConfiguration
nodeId={nodeId}
data={data}
onClose={onClose}
onConfigChange={handleConfigChange}
/>
);
}
const handleLabelChange = (label: string) => {
const currentLabel = currentData.label || nodeId;
// If the label hasn't changed, do nothing
if (label === currentLabel) {
return;
}
// Check if there are any dependencies on this label
if (currentWorkflow) {
const dependencies = LabelDependencyManager.findLabelDependencies(
currentWorkflow,
nodeId,
currentLabel
);
if (dependencies.length > 0) {
// Show dialog to handle dependencies
setPendingLabel(label);
setShowLabelDialog(true);
return;
}
}
// No dependencies, safe to update directly
updateNode(nodeId, { label });
};
const handleLabelChangeConfirm = (
newLabel: string,
updateReferences: boolean
) => {
if (!currentWorkflow) return;
// Use the new store method that handles dependencies
updateNodeLabelWithDependencies(nodeId, newLabel, updateReferences);
setShowLabelDialog(false);
setPendingLabel("");
};
const handleLabelChangeCancel = () => {
setShowLabelDialog(false);
setPendingLabel("");
};
const handleOptimizePrompt = async () => {
if (!currentData.config.prompt) {
alert("Please enter a prompt first");
return;
}
setIsOptimizing(true);
setOptimizationResult("");
try {
// Extract intent from the prompt itself
const prompt = currentData.config.prompt;
const entities = {
aiTasks: prompt.toLowerCase().includes("analyze")
? ["analyze"]
: prompt.toLowerCase().includes("summarize")
? ["summarize"]
: prompt.toLowerCase().includes("extract")
? ["extract"]
: prompt.toLowerCase().includes("classify")
? ["classify"]
: prompt.toLowerCase().includes("generate")
? ["generate"]
: ["process"],
dataTypes: prompt.toLowerCase().includes("resume")
? ["resume"]
: prompt.toLowerCase().includes("document")
? ["document"]
: prompt.toLowerCase().includes("text")
? ["text"]
: ["text"],
urls: [],
complexity: "medium",
};
// Create mock node context
const nodeContext = {
dataType: "text",
previousNodes: [],
intent: "AI_ANALYSIS",
domain: prompt.toLowerCase().includes("resume")
? "jobApplication"
: prompt.toLowerCase().includes("financial")
? "financial"
: prompt.toLowerCase().includes("legal")
? "legal"
: "general",
workflowType: "ai_analysis",
availableData: new Map(),
};
// Generate optimized prompt using the prompt optimizer
const optimizedPrompt = promptOptimizer.generateOptimizedPrompt(
prompt,
entities,
nodeContext,
new Map()
);
// Make DeepSeek API call to further optimize the prompt
const apiResponse = await callOpenAI(
`You are a prompt optimization expert. Your task is to optimize the given prompt for better AI performance.
IMPORTANT: Return ONLY the optimized prompt. Do not include any explanations, comments, or additional text. Just the optimized prompt itself.
Original Prompt: ${prompt}
Optimized Template: ${optimizedPrompt}
Return only the optimized prompt:`,
{
model: "deepseek-chat",
temperature: 0.7,
maxTokens: 1000,
}
);
// Clean up the response to ensure we only get the optimized prompt
let cleanedResult = apiResponse.content.trim();
// Remove common prefixes that might be added by the AI
const prefixesToRemove = [
"Optimized Prompt:",
"Here's the optimized prompt:",
"The optimized prompt is:",
"Optimized version:",
"Here is the optimized prompt:",
"Optimized prompt:",
"Here's the improved prompt:",
"Improved prompt:",
"Here is the improved prompt:",
"The improved prompt is:",
"Here's the enhanced prompt:",
"Enhanced prompt:",
"Here is the enhanced prompt:",
"The enhanced prompt is:",
];
for (const prefix of prefixesToRemove) {
if (cleanedResult.toLowerCase().startsWith(prefix.toLowerCase())) {
cleanedResult = cleanedResult.substring(prefix.length).trim();
}
}
// Remove any quotes that might wrap the prompt
if (
(cleanedResult.startsWith('"') && cleanedResult.endsWith('"')) ||
(cleanedResult.startsWith("'") && cleanedResult.endsWith("'"))
) {
cleanedResult = cleanedResult.slice(1, -1).trim();
}
setOptimizationResult(cleanedResult);
} catch (error) {
console.error("Error optimizing prompt:", error);
setOptimizationResult("Error optimizing prompt. Please try again.");
} finally {
setIsOptimizing(false);
}
};
const applyOptimizedPrompt = () => {
if (optimizationResult) {
handleConfigChange("prompt", optimizationResult);
setOptimizationResult("");
}
};
const renderField = (field: FieldConfig) => {
const value = currentData.config[field.key] || field.defaultValue || "";
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}
nodeType={data.type}
/>
{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>
)}
</div>
);
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="w-full max-w-md max-h-[80vh] overflow-hidden">
<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" />
<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}
variant="ghost"
size="icon"
className="h-8 w-8"
>
<X className="w-4 h-4" />
</Button>
</CardHeader>
<CardContent className="space-y-4 overflow-y-auto max-h-[60vh]">
<FieldWithHelp
fieldName="label"
value={currentData.label}
onChange={handleLabelChange}
type="text"
placeholder="Enter node label"
nodeType={data.type}
/>
{config.fields.map((field) => (
<div key={field.key}>{renderField(field)}</div>
))}
{/* Optimization Result Display */}
{data.type === "llmTask" && optimizationResult && (
<Card className="mt-4 bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200">
<CardHeader className="pb-3">
<div className="flex items-center space-x-2">
<Sparkles className="w-4 h-4 text-purple-600" />
<CardTitle className="text-sm font-semibold text-purple-800">
Optimized Prompt Preview
</CardTitle>
<Badge variant="info" className="text-xs">
Preview
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="bg-white p-4 rounded-md border border-purple-100 shadow-sm">
<div className="mb-2 text-xs text-gray-500 font-medium">
Optimized Prompt:
</div>
<pre className="text-sm text-gray-800 whitespace-pre-wrap font-mono leading-relaxed bg-gray-50 p-3 rounded border">
{optimizationResult}
</pre>
</div>
<div className="mt-3 flex items-center justify-between">
<div className="text-xs text-purple-600">
Review the optimized prompt above and click "Apply" to
replace your current prompt.
</div>
<div className="flex space-x-2">
<Button
onClick={() => setOptimizationResult("")}
variant="ghost"
size="sm"
className="text-xs"
>
Cancel
</Button>
<Button
onClick={applyOptimizedPrompt}
size="sm"
className="bg-green-500 hover:bg-green-600 text-xs"
>
Apply Optimized Prompt
</Button>
</div>
</div>
</CardContent>
</Card>
)}
</CardContent>
<div className="flex justify-end space-x-2 p-4 border-t border-gray-200">
<Button onClick={onClose} variant="secondary">
Close
</Button>
</div>
</Card>
{/* Label Change Dialog */}
{showLabelDialog && currentWorkflow && (
<LabelChangeDialog
isOpen={showLabelDialog}
onClose={handleLabelChangeCancel}
onConfirm={handleLabelChangeConfirm}
nodeId={nodeId}
oldLabel={currentData.label || nodeId}
newLabel={pendingLabel}
workflow={currentWorkflow}
/>
)}
</div>
);
};