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:
Nikhil-Doye
2025-10-27 19:19:02 -04:00
parent 7163a2a1d2
commit 6cd8d7c5b6
2 changed files with 136 additions and 483 deletions
+136 -47
View File
@@ -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/)