chore(dev): workflow ui improvement

This commit is contained in:
Soumyadas15 2025-06-15 16:10:58 +05:30
parent d13d6a55e6
commit e2fc2478cc
5 changed files with 185 additions and 96 deletions

View File

@ -231,7 +231,13 @@ class WorkflowConverter:
tool_data = tool_node.get('data', {}) tool_data = tool_node.get('data', {})
tool_name = tool_data.get('nodeId', tool_data.get('label', 'Unknown Tool')) tool_name = tool_data.get('nodeId', tool_data.get('label', 'Unknown Tool'))
tool_desc = tool_data.get('description', 'No description available') tool_desc = tool_data.get('description', 'No description available')
prompt_parts.append(f"- **{tool_name}**: {tool_desc}") tool_instructions = tool_data.get('instructions', '')
# Build tool description with instructions if provided
tool_description = f"- **{tool_name}**: {tool_desc}"
if tool_instructions:
tool_description += f" - Instructions: {tool_instructions}"
prompt_parts.append(tool_description)
# Collect tool ID for XML examples # Collect tool ID for XML examples
tool_id = tool_data.get('nodeId') tool_id = tool_data.get('nodeId')
@ -327,6 +333,7 @@ class WorkflowConverter:
data = node.get('data', {}) data = node.get('data', {})
name = data.get('label', 'Tool') name = data.get('label', 'Tool')
tool_id = data.get('nodeId', 'unknown_tool') tool_id = data.get('nodeId', 'unknown_tool')
instructions = data.get('instructions', '')
input_connections = self._find_node_inputs(node.get('id'), edges) input_connections = self._find_node_inputs(node.get('id'), edges)
output_connections = self._find_node_outputs(node.get('id'), edges) output_connections = self._find_node_outputs(node.get('id'), edges)
@ -337,6 +344,10 @@ class WorkflowConverter:
f"**Purpose**: Provides {name.lower()} functionality to the workflow", f"**Purpose**: Provides {name.lower()} functionality to the workflow",
] ]
# Add instructions if provided
if instructions:
description.append(f"**Instructions**: {instructions}")
if input_connections: if input_connections:
description.append(f"**Connected to agents**: {', '.join(input_connections)}") description.append(f"**Connected to agents**: {', '.join(input_connections)}")

View File

@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Plus, Play, Edit, Trash2, Clock, CheckCircle, XCircle, AlertCircle, Check, X } from "lucide-react"; import { Plus, Play, Edit, Trash2, Clock, CheckCircle, XCircle, AlertCircle, Check, X, Workflow as WorkflowIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@ -234,6 +234,23 @@ export default function WorkflowsPage() {
} }
}; };
const getWorkflowColor = (status: string) => {
switch (status) {
case "active":
return "#10b981"; // green-500
case "draft":
return "#f59e0b"; // amber-500
case "paused":
return "#6b7280"; // gray-500
case "disabled":
return "#ef4444"; // red-500
case "archived":
return "#9ca3af"; // gray-400
default:
return "#8b5cf6"; // violet-500
}
};
if (loading) { if (loading) {
return ( return (
<div className="max-w-7xl mx-auto p-6 space-y-6"> <div className="max-w-7xl mx-auto p-6 space-y-6">
@ -322,91 +339,96 @@ export default function WorkflowsPage() {
) : ( ) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{workflows.map((workflow) => ( {workflows.map((workflow) => (
<Card key={workflow.id}> <div
<CardHeader> key={workflow.id}
<div className="flex justify-between items-start"> className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 group"
<div className="space-y-1 flex-1 mr-2"> >
{editingWorkflowId === workflow.id ? ( {/* Colorful Header */}
<div className="flex items-center gap-2"> <div
<Input className="h-24 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100"
value={editingName} style={{ backgroundColor: getWorkflowColor(workflow.status) }}
onChange={(e) => setEditingName(e.target.value)} >
className="text-lg font-semibold h-8" <div className="text-3xl text-white drop-shadow-sm">
onKeyDown={(e) => { <WorkflowIcon />
if (e.key === 'Enter') {
handleSaveEditName(workflow.id);
} else if (e.key === 'Escape') {
handleCancelEditName();
}
}}
autoFocus
/>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => handleSaveEditName(workflow.id)}
disabled={updatingWorkflows.has(workflow.id)}
>
{updatingWorkflows.has(workflow.id) ? (
<div className="animate-spin rounded-full h-3 w-3 border-b border-current" />
) : (
<Check className="h-3 w-3 text-green-600" />
)}
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={handleCancelEditName}
disabled={updatingWorkflows.has(workflow.id)}
>
<X className="h-3 w-3 text-red-600" />
</Button>
</div>
</div>
) : (
<CardTitle
className="text-lg cursor-pointer hover:text-primary transition-colors"
onClick={() => handleStartEditName(workflow)}
>
{workflow.definition.name || workflow.name}
</CardTitle>
)}
<CardDescription>{workflow.definition.description || workflow.description}</CardDescription>
</div>
{getStatusBadge(workflow.status)}
</div> </div>
</CardHeader> <div className="absolute top-3 right-3">
<CardContent> {getStatusIcon(workflow.status)}
<div className="space-y-4"> </div>
</div>
{/* Content */}
<div className="p-4">
<div className="space-y-3">
{/* Title and Status */}
<div className="flex justify-between items-start gap-2">
<div className="flex-1 min-w-0">
{editingWorkflowId === workflow.id ? (
<div className="flex items-center gap-2">
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
className="text-lg font-semibold h-8"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSaveEditName(workflow.id);
} else if (e.key === 'Escape') {
handleCancelEditName();
}
}}
autoFocus
/>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => handleSaveEditName(workflow.id)}
disabled={updatingWorkflows.has(workflow.id)}
>
{updatingWorkflows.has(workflow.id) ? (
<div className="animate-spin rounded-full h-3 w-3 border-b border-current" />
) : (
<Check className="h-3 w-3 text-green-600" />
)}
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={handleCancelEditName}
disabled={updatingWorkflows.has(workflow.id)}
>
<X className="h-3 w-3 text-red-600" />
</Button>
</div>
</div>
) : (
<h3
className="text-foreground font-medium text-lg line-clamp-1 cursor-pointer hover:text-primary transition-colors"
onClick={() => handleStartEditName(workflow)}
>
{workflow.definition.name || workflow.name}
</h3>
)}
</div>
{getStatusBadge(workflow.status)}
</div>
<p className="text-muted-foreground text-sm line-clamp-2 min-h-[2.5rem]">
{workflow.definition.description || workflow.description || 'No description provided'}
</p>
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<Link href={`/workflows/builder/${workflow.id}`} className="flex-1"> <Link href={`/workflows/builder/${workflow.id}`} className="flex-1">
<Button variant="outline" size="sm" className="w-full"> <Button variant="outline" size="sm" className="w-full">
<Edit className="mr-2 h-3 w-3" /> <Edit className="h-3 w-3" />
Edit Edit
</Button> </Button>
</Link> </Link>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => handleRunWorkflow(workflow.id)}
disabled={executingWorkflows.has(workflow.id) || workflow.status !== 'active'}
>
{executingWorkflows.has(workflow.id) ? (
<div className="animate-spin rounded-full h-3 w-3 border-b border-current mr-2" />
) : (
<Play className="mr-2 h-3 w-3" />
)}
Run
</Button>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity"
disabled={deletingWorkflows.has(workflow.id)} disabled={deletingWorkflows.has(workflow.id)}
> >
{deletingWorkflows.has(workflow.id) ? ( {deletingWorkflows.has(workflow.id) ? (
@ -428,7 +450,7 @@ export default function WorkflowsPage() {
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => handleDeleteWorkflow(workflow.id)} onClick={() => handleDeleteWorkflow(workflow.id)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-white hover:bg-destructive/90"
> >
Delete Workflow Delete Workflow
</AlertDialogAction> </AlertDialogAction>
@ -437,8 +459,8 @@ export default function WorkflowsPage() {
</AlertDialog> </AlertDialog>
</div> </div>
</div> </div>
</CardContent> </div>
</Card> </div>
))} ))}
</div> </div>
)} )}

View File

@ -213,15 +213,15 @@ export default function NodePalette() {
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}> <Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="input"> <TabsTrigger value="input">
<Play className="h-4 w-4 mr-2" /> <Play className="h-4 w-4" />
Input Input
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="agent"> <TabsTrigger value="agent">
<Bot className="h-4 w-4 mr-2" /> <Bot className="h-4 w-4" />
Agents Agents
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="tools"> <TabsTrigger value="tools">
<Wrench className="h-4 w-4 mr-2" /> <Wrench className="h-4 w-4" />
Tools Tools
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@ -257,8 +257,8 @@ export default function NodePalette() {
variables: {} variables: {}
}} }}
> >
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move border-primary/30 bg-primary/5"> <Card className="py-2 group transition-all duration-200 border hover:border-primary/50 cursor-move border-primary/30 bg-primary/5">
<CardHeader className="p-4 py-0"> <CardContent className="p-2 py-0">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-primary/20 border border-primary/30 group-hover:bg-primary/30 transition-colors"> <div className="p-2 rounded-lg bg-primary/20 border border-primary/30 group-hover:bg-primary/30 transition-colors">
<Icon className="h-5 w-5 text-primary" /> <Icon className="h-5 w-5 text-primary" />
@ -268,12 +268,12 @@ export default function NodePalette() {
{node.name} {node.name}
<Badge variant="outline" className="text-xs">Required</Badge> <Badge variant="outline" className="text-xs">Required</Badge>
</CardTitle> </CardTitle>
<CardDescription className="text-xs mt-1 line-clamp-2"> <CardDescription className="text-xs line-clamp-2">
{node.description} {node.description}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardContent>
</Card> </Card>
</DraggableNode> </DraggableNode>
); );
@ -310,9 +310,9 @@ export default function NodePalette() {
config: {}, config: {},
}} }}
> >
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move"> <Card className="py-2 group transition-all duration-200 border hover:border-primary/50 cursor-move">
<CardHeader className="p-4 py-0"> <CardContent className="p-2 py-0">
<div className="flex items-start gap-3"> <div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20 group-hover:bg-primary/20 transition-colors"> <div className="p-2 rounded-lg bg-primary/10 border border-primary/20 group-hover:bg-primary/20 transition-colors">
<Icon className="h-5 w-5 text-primary" /> <Icon className="h-5 w-5 text-primary" />
</div> </div>
@ -320,12 +320,12 @@ export default function NodePalette() {
<CardTitle className="text-sm font-semibold leading-tight"> <CardTitle className="text-sm font-semibold leading-tight">
{node.name} {node.name}
</CardTitle> </CardTitle>
<CardDescription className="text-xs mt-1 line-clamp-2"> <CardDescription className="text-xs line-clamp-2">
{node.description} {node.description}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardContent>
</Card> </Card>
</DraggableNode> </DraggableNode>
); );
@ -361,10 +361,11 @@ export default function NodePalette() {
label: node.name, label: node.name,
nodeId: node.id, nodeId: node.id,
config: {}, config: {},
instructions: "",
}} }}
> >
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move"> <Card className="py-2 group transition-all duration-200 border hover:border-primary/50 cursor-move">
<CardHeader className="p-4 py-0"> <CardContent className="p-2 py-0">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20 group-hover:bg-primary/20 transition-colors"> <div className="p-2 rounded-lg bg-primary/10 border border-primary/20 group-hover:bg-primary/20 transition-colors">
<Icon className="h-5 w-5 text-primary" /> <Icon className="h-5 w-5 text-primary" />
@ -378,7 +379,7 @@ export default function NodePalette() {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardContent>
</Card> </Card>
</DraggableNode> </DraggableNode>
); );

View File

@ -84,6 +84,7 @@ const initialNodes: Node[] = [
data: { data: {
label: 'Web Search', label: 'Web Search',
nodeId: 'web_search_tool', nodeId: 'web_search_tool',
instructions: '',
outputConnections: [ outputConnections: [
{ {
id: 'agent-1', id: 'agent-1',

View File

@ -1,9 +1,14 @@
"use client"; "use client";
import { memo } from "react"; import { memo, useState } from "react";
import { Handle, Position, NodeProps } from "@xyflow/react"; import { Handle, Position, NodeProps } from "@xyflow/react";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { import {
Database, Database,
FileText, FileText,
@ -14,18 +19,24 @@ import {
Cloud, Cloud,
Send, Send,
Settings, Settings,
Wrench Wrench,
ChevronDown,
ChevronUp
} from "lucide-react"; } from "lucide-react";
import { useWorkflow } from "../WorkflowContext";
interface ToolConnectionNodeData { interface ToolConnectionNodeData {
label: string; label: string;
nodeId?: string; nodeId?: string;
toolType?: string; toolType?: string;
config?: any; config?: any;
instructions?: string;
} }
const ToolConnectionNode = memo(({ data, selected }: NodeProps) => { const ToolConnectionNode = memo(({ data, selected, id }: NodeProps) => {
const nodeData = data as unknown as ToolConnectionNodeData; const nodeData = data as unknown as ToolConnectionNodeData;
const [isConfigOpen, setIsConfigOpen] = useState(false);
const { updateNodeData } = useWorkflow();
const getToolConfig = () => { const getToolConfig = () => {
const toolId = nodeData.nodeId || nodeData.toolType; const toolId = nodeData.nodeId || nodeData.toolType;
@ -134,6 +145,49 @@ const ToolConnectionNode = memo(({ data, selected }: NodeProps) => {
<span className="text-xs text-muted-foreground">Ready</span> <span className="text-xs text-muted-foreground">Ready</span>
</div> </div>
</div> </div>
{/* Show instructions preview if available */}
{nodeData.instructions && !isConfigOpen && (
<div className="text-xs text-muted-foreground bg-muted/50 p-2 rounded border">
<strong>Instructions:</strong> {nodeData.instructions.length > 50
? `${nodeData.instructions.substring(0, 50)}...`
: nodeData.instructions}
</div>
)}
<Separator />
{/* Configuration Toggle */}
<Collapsible open={isConfigOpen} onOpenChange={setIsConfigOpen}>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-full justify-between p-2">
<span className="flex items-center gap-2">
<Settings className="h-4 w-4" />
Configure
</span>
{isConfigOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-3">
{/* Instructions Configuration */}
<div className="space-y-2">
<Label htmlFor={`instructions-${id}`} className="text-sm font-medium">
Tool Instructions
</Label>
<Textarea
id={`instructions-${id}`}
placeholder="Provide specific instructions for how this tool should be used in the workflow..."
value={nodeData.instructions || ''}
onChange={(e) => updateNodeData?.(id!, { instructions: e.target.value })}
className="min-h-[80px] text-sm"
/>
<p className="text-xs text-muted-foreground">
These instructions will be included in the workflow prompt to guide how the agent uses this tool.
</p>
</div>
</CollapsibleContent>
</Collapsible>
</CardContent> </CardContent>
</Card> </Card>