mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
Enhance WorkflowList component with node type summary and visual indicators
- Added helper functions to retrieve node icons and colors based on node types, improving visual representation in the workflow list. - Implemented a summary section displaying node type counts and breakdowns, enhancing user understanding of workflow composition. - Refactored existing node type indicators to utilize the new helper functions for consistency and maintainability. - Removed outdated code related to previous node type handling, streamlining the component's logic.
This commit is contained in:
+136
-47
@@ -17,6 +17,12 @@ import {
|
||||
ArrowRight,
|
||||
Clock,
|
||||
HelpCircle,
|
||||
Database,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
FileText,
|
||||
Search,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
|
||||
interface WorkflowListProps {
|
||||
@@ -24,6 +30,88 @@ interface WorkflowListProps {
|
||||
onCreateWorkflow: (workflowName?: string) => void;
|
||||
}
|
||||
|
||||
// Helper function to get node icon
|
||||
const getNodeIconComponent = (type: string) => {
|
||||
switch (type) {
|
||||
case "dataInput":
|
||||
return ArrowRight;
|
||||
case "dataOutput":
|
||||
return ArrowRight;
|
||||
case "webScraping":
|
||||
return Globe;
|
||||
case "llmTask":
|
||||
return Brain;
|
||||
case "embeddingGenerator":
|
||||
return Sparkles;
|
||||
case "similaritySearch":
|
||||
return Search;
|
||||
case "structuredOutput":
|
||||
return FileText;
|
||||
case "database":
|
||||
return Database;
|
||||
case "slack":
|
||||
return MessageSquare;
|
||||
case "discord":
|
||||
return MessageSquare;
|
||||
case "gmail":
|
||||
return Mail;
|
||||
default:
|
||||
return Zap;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get node color
|
||||
const getNodeColorClass = (type: string) => {
|
||||
switch (type) {
|
||||
case "dataInput":
|
||||
return "bg-blue-100 text-blue-700 border-blue-200";
|
||||
case "dataOutput":
|
||||
return "bg-orange-100 text-orange-700 border-orange-200";
|
||||
case "webScraping":
|
||||
return "bg-green-100 text-green-700 border-green-200";
|
||||
case "llmTask":
|
||||
return "bg-purple-100 text-purple-700 border-purple-200";
|
||||
case "embeddingGenerator":
|
||||
return "bg-pink-100 text-pink-700 border-pink-200";
|
||||
case "similaritySearch":
|
||||
return "bg-indigo-100 text-indigo-700 border-indigo-200";
|
||||
case "structuredOutput":
|
||||
return "bg-yellow-100 text-yellow-700 border-yellow-200";
|
||||
case "database":
|
||||
return "bg-cyan-100 text-cyan-700 border-cyan-200";
|
||||
case "slack":
|
||||
return "bg-violet-100 text-violet-700 border-violet-200";
|
||||
case "discord":
|
||||
return "bg-indigo-100 text-indigo-700 border-indigo-200";
|
||||
case "gmail":
|
||||
return "bg-red-100 text-red-700 border-red-200";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-700 border-gray-200";
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to format node type name
|
||||
const formatNodeTypeName = (type: string): string => {
|
||||
const formatted = type.replace(/([A-Z])/g, " $1").trim();
|
||||
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
||||
};
|
||||
|
||||
// Helper function to get node type summary with counts
|
||||
const getNodeTypeSummary = (nodes: any[]) => {
|
||||
const typeCounts = new Map<string, number>();
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const count = typeCounts.get(node.type) || 0;
|
||||
typeCounts.set(node.type, count + 1);
|
||||
});
|
||||
|
||||
// Sort by count (descending) and then alphabetically
|
||||
return Array.from(typeCounts.entries()).sort((a, b) => {
|
||||
if (b[1] !== a[1]) return b[1] - a[1]; // Sort by count first
|
||||
return a[0].localeCompare(b[0]); // Then alphabetically
|
||||
});
|
||||
};
|
||||
|
||||
export const WorkflowList: React.FC<WorkflowListProps> = ({
|
||||
onOpenWorkflow,
|
||||
onCreateWorkflow,
|
||||
@@ -397,56 +485,57 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node Type Indicators */}
|
||||
<div className="flex flex-wrap gap-1 mb-4">
|
||||
{workflow.nodes.slice(0, 3).map((node, index) => {
|
||||
const getNodeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "dataInput":
|
||||
return <ArrowRight className="w-3 h-3" />;
|
||||
case "webScraping":
|
||||
return <Globe className="w-3 h-3" />;
|
||||
case "llmTask":
|
||||
return <Brain className="w-3 h-3" />;
|
||||
case "dataOutput":
|
||||
return <ArrowRight className="w-3 h-3" />;
|
||||
default:
|
||||
return <Zap className="w-3 h-3" />;
|
||||
}
|
||||
};
|
||||
const getNodeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "dataInput":
|
||||
return "bg-blue-100 text-blue-600";
|
||||
case "webScraping":
|
||||
return "bg-green-100 text-green-600";
|
||||
case "llmTask":
|
||||
return "bg-purple-100 text-purple-600";
|
||||
case "dataOutput":
|
||||
return "bg-orange-100 text-orange-600";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-600";
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium ${getNodeColor(
|
||||
node.type
|
||||
)}`}
|
||||
>
|
||||
{getNodeIcon(node.type)}
|
||||
<span>
|
||||
{node.type.replace(/([A-Z])/g, " $1").trim()}
|
||||
{/* Node Type Summary with Counts */}
|
||||
<div className="space-y-2 mb-4">
|
||||
{/* Workflow Stats */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 rounded-lg border border-gray-100">
|
||||
<div className="flex items-center space-x-4 text-xs">
|
||||
<div className="flex items-center space-x-1 text-gray-600">
|
||||
<GitBranch className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">
|
||||
{workflow.nodes.length}
|
||||
</span>
|
||||
<span className="text-gray-400">nodes</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1 text-gray-600">
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">
|
||||
{workflow.edges.length}
|
||||
</span>
|
||||
<span className="text-gray-400">connections</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{workflow.nodes.length > 3 && (
|
||||
<div className="flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600">
|
||||
<span>+{workflow.nodes.length - 3} more</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs font-medium text-gray-500">
|
||||
{getNodeTypeSummary(workflow.nodes).length}{" "}
|
||||
{getNodeTypeSummary(workflow.nodes).length === 1
|
||||
? "type"
|
||||
: "types"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node Type Breakdown */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{getNodeTypeSummary(workflow.nodes).map(([type, count]) => {
|
||||
const IconComponent = getNodeIconComponent(type);
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
className={`flex items-center space-x-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium border ${getNodeColorClass(
|
||||
type
|
||||
)} transition-all duration-200 hover:shadow-sm`}
|
||||
title={`${count} ${formatNodeTypeName(type)} node${
|
||||
count > 1 ? "s" : ""
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="w-3.5 h-3.5" />
|
||||
<span className="font-semibold">{count}</span>
|
||||
<span className="hidden sm:inline">
|
||||
{formatNodeTypeName(type)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
# Robust JSON Parsing for LLM Outputs
|
||||
|
||||
## Overview
|
||||
|
||||
LLM outputs can occasionally generate malformed JSON due to:
|
||||
|
||||
- Code block wrappers (`json ... `)
|
||||
- Trailing commas in objects/arrays
|
||||
- Extra whitespace or newlines
|
||||
- Markdown formatting
|
||||
- Incomplete JSON (truncated responses)
|
||||
- Single quotes instead of double quotes
|
||||
- Missing quotes around property names
|
||||
- Comments in JSON
|
||||
|
||||
This solution provides a multi-strategy JSON parser that can recover from these common issues.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **`parseRobustJson<T>(input, options)`**
|
||||
|
||||
- Main parsing function with multiple fallback strategies
|
||||
- Returns a `ParseResult<T>` with success status and data/error
|
||||
|
||||
2. **`parseAndValidate<T>(input, requiredFields, options)`**
|
||||
|
||||
- Combines parsing with structure validation
|
||||
- Ensures parsed JSON has expected fields and types
|
||||
|
||||
3. **Parsing Strategies** (applied in order):
|
||||
- **Direct Parse**: Fast path for valid JSON
|
||||
- **Cleaned Parse**: Remove code blocks, markdown, BOM
|
||||
- **Advanced Recovery**: Fix trailing commas, quotes, comments
|
||||
- **Relaxed Parse**: Extract JSON from partial/broken input
|
||||
|
||||
### Parsing Strategy Details
|
||||
|
||||
#### Strategy 1: Direct Parse
|
||||
|
||||
```typescript
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
/* fallback */
|
||||
}
|
||||
```
|
||||
|
||||
- Fastest path for valid JSON
|
||||
- No modifications to input
|
||||
|
||||
#### Strategy 2: Cleaned Parse
|
||||
|
||||
````typescript
|
||||
const cleaned = cleanJsonString(input);
|
||||
// Removes:
|
||||
// - Code blocks: ```json ... ```
|
||||
// - Markdown formatting
|
||||
// - BOM characters
|
||||
// - Extra whitespace
|
||||
return JSON.parse(cleaned);
|
||||
````
|
||||
|
||||
#### Strategy 3: Advanced Recovery
|
||||
|
||||
```typescript
|
||||
const recovered = advancedJsonRecovery(input);
|
||||
// Fixes:
|
||||
// - Trailing commas: ,} → }
|
||||
// - Missing quotes: {name: "val"} → {"name": "val"}
|
||||
// - Single quotes: 'value' → "value"
|
||||
// - Comments: // ... or /* ... */
|
||||
return JSON.parse(recovered);
|
||||
```
|
||||
|
||||
#### Strategy 4: Relaxed Parse
|
||||
|
||||
```typescript
|
||||
// Extract JSON boundaries
|
||||
// Manually parse key-value pairs
|
||||
// Return best-effort object
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Updated Files
|
||||
|
||||
1. **`src/services/naturalLanguageToWorkflowService.ts`**
|
||||
- `classifyIntent()`: Line ~145
|
||||
- `extractEntities()`: Line ~238
|
||||
- `generateWorkflowStructure()`: Line ~416
|
||||
|
||||
### Before (Fragile)
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await callOpenAI(prompt, config);
|
||||
const result = JSON.parse(response.content); // ❌ Can crash
|
||||
return result;
|
||||
} catch (error) {
|
||||
return fallback();
|
||||
}
|
||||
```
|
||||
|
||||
### After (Robust)
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await callOpenAI(prompt, config);
|
||||
|
||||
// ✅ Robust parsing with validation
|
||||
const parseResult = parseAndValidate(
|
||||
response.content,
|
||||
[
|
||||
{ field: "intent", type: "string" },
|
||||
{ field: "confidence", type: "number" },
|
||||
],
|
||||
{ logAttempts: true }
|
||||
);
|
||||
|
||||
if (!parseResult.success) {
|
||||
console.error("Parse failed:", parseResult.error);
|
||||
throw new Error(parseResult.error);
|
||||
}
|
||||
|
||||
return parseResult.data;
|
||||
} catch (error) {
|
||||
return fallback();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseRobustJson<T>(input, options)`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `input: string` - JSON string to parse (possibly malformed)
|
||||
- `options`:
|
||||
- `strictMode?: boolean` - Skip relaxed parsing (default: false)
|
||||
- `fallbackValue?: T` - Return value if all strategies fail
|
||||
- `logAttempts?: boolean` - Log parsing attempts (default: false)
|
||||
|
||||
**Returns:** `ParseResult<T>`
|
||||
|
||||
```typescript
|
||||
interface ParseResult<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
strategy?: string; // "direct" | "cleaned" | "recovered" | "relaxed" | "fallback"
|
||||
cleanedJson?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const result = parseRobustJson(llmOutput, {
|
||||
logAttempts: true,
|
||||
fallbackValue: { default: "structure" },
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log("Parsed:", result.data);
|
||||
console.log("Strategy used:", result.strategy);
|
||||
} else {
|
||||
console.error("Failed:", result.error);
|
||||
}
|
||||
```
|
||||
|
||||
### `parseAndValidate<T>(input, requiredFields, options)`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `input: string` - JSON string to parse
|
||||
- `requiredFields: Array<{ field: string; type: string }>` - Expected structure
|
||||
- `options`: Same as `parseRobustJson`
|
||||
|
||||
**Returns:** `ParseResult<T> & { validationErrors?: string[] }`
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const result = parseAndValidate(
|
||||
llmOutput,
|
||||
[
|
||||
{ field: "intent", type: "string" },
|
||||
{ field: "confidence", type: "number" },
|
||||
{ field: "nodes", type: "object" },
|
||||
],
|
||||
{
|
||||
logAttempts: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Validation errors:", result.validationErrors);
|
||||
}
|
||||
```
|
||||
|
||||
### `validateJsonStructure(data, requiredFields, options)`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `data: any` - Parsed JSON data
|
||||
- `requiredFields: Array<{ field: string; type: string }>`
|
||||
- `options`:
|
||||
- `allowExtra?: boolean` - Allow extra fields not in schema
|
||||
|
||||
**Returns:** `{ isValid: boolean; errors: string[] }`
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Handling Code Blocks
|
||||
|
||||
```typescript
|
||||
const llmOutput = `
|
||||
\`\`\`json
|
||||
{
|
||||
"intent": "web_scraping",
|
||||
"confidence": 0.95
|
||||
}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const result = parseRobustJson(llmOutput);
|
||||
// ✅ Success: { intent: "web_scraping", confidence: 0.95 }
|
||||
```
|
||||
|
||||
### Example 2: Handling Trailing Commas
|
||||
|
||||
```typescript
|
||||
const llmOutput = `{
|
||||
"intent": "data_analysis",
|
||||
"confidence": 0.87,
|
||||
"nodes": ["input", "process", "output",],
|
||||
}`;
|
||||
|
||||
const result = parseRobustJson(llmOutput);
|
||||
// ✅ Success: Trailing commas fixed automatically
|
||||
```
|
||||
|
||||
### Example 3: Handling Missing Quotes
|
||||
|
||||
```typescript
|
||||
const llmOutput = `{
|
||||
intent: "workflow_generation",
|
||||
confidence: 0.92
|
||||
}`;
|
||||
|
||||
const result = parseRobustJson(llmOutput);
|
||||
// ✅ Success: Property names quoted automatically
|
||||
```
|
||||
|
||||
### Example 4: Validation with Required Fields
|
||||
|
||||
```typescript
|
||||
const result = parseAndValidate(llmOutput, [
|
||||
{ field: "intent", type: "string" },
|
||||
{ field: "confidence", type: "number" },
|
||||
]);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Missing required fields:", result.validationErrors);
|
||||
// e.g., ["Missing required field: intent"]
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### 1. Increased Reliability
|
||||
|
||||
- **Before**: ~5-10% failure rate due to malformed JSON
|
||||
- **After**: <1% failure rate with multi-strategy parsing
|
||||
|
||||
### 2. Better Error Messages
|
||||
|
||||
- **Before**: Generic "Unexpected token" errors
|
||||
- **After**: Clear indication of which parsing strategy was used/failed
|
||||
|
||||
### 3. Graceful Degradation
|
||||
|
||||
- Falls back through multiple strategies before giving up
|
||||
- Can return fallback values for non-critical failures
|
||||
|
||||
### 4. User Trust
|
||||
|
||||
- Fewer "something went wrong" errors
|
||||
- More reliable AI-powered features
|
||||
- Better developer experience
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
````typescript
|
||||
// Test code block removal
|
||||
expect(parseRobustJson('```json\n{"key":"value"}\n```')).toMatchObject({
|
||||
success: true,
|
||||
strategy: "cleaned",
|
||||
data: { key: "value" },
|
||||
});
|
||||
|
||||
// Test trailing comma fix
|
||||
expect(parseRobustJson('{"key":"value",}')).toMatchObject({
|
||||
success: true,
|
||||
strategy: "recovered",
|
||||
data: { key: "value" },
|
||||
});
|
||||
|
||||
// Test validation
|
||||
expect(
|
||||
parseAndValidate('{"key":"value"}', [{ field: "key", type: "string" }])
|
||||
).toMatchObject({
|
||||
success: true,
|
||||
validationErrors: undefined,
|
||||
});
|
||||
````
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Test with actual LLM outputs from DeepSeek/OpenAI
|
||||
- Verify fallback behavior on complete failures
|
||||
- Ensure performance is acceptable (< 10ms per parse)
|
||||
|
||||
## Performance
|
||||
|
||||
### Benchmarks
|
||||
|
||||
- **Direct parse** (valid JSON): ~0.1ms
|
||||
- **Cleaned parse**: ~0.5ms
|
||||
- **Advanced recovery**: ~2ms
|
||||
- **Relaxed parse**: ~5ms
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. Use `strictMode: true` when JSON is likely to be valid
|
||||
2. Cache parsing results for repeated inputs
|
||||
3. Set appropriate `maxTokens` to prevent large responses
|
||||
4. Use `logAttempts: false` in production for better performance
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
1. **Schema-based validation**: Support JSON Schema for complex validation
|
||||
2. **Custom recovery rules**: Allow users to define custom cleanup patterns
|
||||
3. **Streaming parser**: Handle large JSON streams incrementally
|
||||
4. **Type inference**: Automatically infer TypeScript types from parsed data
|
||||
5. **Metrics collection**: Track parsing success rates and strategies used
|
||||
|
||||
### LLM Prompt Engineering
|
||||
|
||||
To reduce malformed JSON from LLMs:
|
||||
|
||||
1. Explicitly request "valid JSON without code blocks"
|
||||
2. Use structured output formats (OpenAI's JSON mode)
|
||||
3. Add validation examples in prompts
|
||||
4. Set lower temperature for more deterministic output
|
||||
5. Use system messages to enforce JSON format
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Existing Code
|
||||
|
||||
1. **Import the parser**:
|
||||
|
||||
```typescript
|
||||
import { parseRobustJson, parseAndValidate } from "./robustJsonParser";
|
||||
```
|
||||
|
||||
2. **Replace JSON.parse()**:
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const data = JSON.parse(llmResponse);
|
||||
|
||||
// After
|
||||
const result = parseRobustJson(llmResponse);
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add validation** (optional but recommended):
|
||||
|
||||
```typescript
|
||||
const result = parseAndValidate(llmResponse, [
|
||||
{ field: "requiredField", type: "string" },
|
||||
]);
|
||||
```
|
||||
|
||||
4. **Handle errors gracefully**:
|
||||
|
||||
```typescript
|
||||
if (!result.success) {
|
||||
console.error("Parse error:", result.error);
|
||||
// Use fallback or retry
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: Parser returns fallback value
|
||||
|
||||
- **Cause**: Input is severely malformed
|
||||
- **Solution**: Check LLM prompt, increase temperature, or use structured output
|
||||
|
||||
**Issue**: Validation fails on correct JSON
|
||||
|
||||
- **Cause**: Type mismatch (e.g., array vs object)
|
||||
- **Solution**: Review required fields, adjust types or make fields optional
|
||||
|
||||
**Issue**: Performance degradation
|
||||
|
||||
- **Cause**: Large inputs triggering multiple strategies
|
||||
- **Solution**: Enable `strictMode`, reduce `maxTokens`, or cache results
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
1. Check console logs with `logAttempts: true`
|
||||
2. Review the parsed/cleaned JSON in error messages
|
||||
3. Test with `parseRobustJson` in isolation
|
||||
4. Consider prompt engineering to improve LLM JSON quality
|
||||
|
||||
## References
|
||||
|
||||
- JSON Specification: [RFC 8259](https://tools.ietf.org/html/rfc8259)
|
||||
- LLM Best Practices: [OpenAI JSON Mode](https://platform.openai.com/docs/guides/text-generation/json-mode)
|
||||
- Error Handling: [Error Handling Best Practices](https://nodejs.org/en/docs/guides/error-handling/)
|
||||
Reference in New Issue
Block a user