mirror of https://github.com/kortix-ai/suna.git
chore(dev): workflow ui improvement
This commit is contained in:
parent
d13d6a55e6
commit
e2fc2478cc
|
@ -231,7 +231,13 @@ class WorkflowConverter:
|
|||
tool_data = tool_node.get('data', {})
|
||||
tool_name = tool_data.get('nodeId', tool_data.get('label', 'Unknown Tool'))
|
||||
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
|
||||
tool_id = tool_data.get('nodeId')
|
||||
|
@ -327,6 +333,7 @@ class WorkflowConverter:
|
|||
data = node.get('data', {})
|
||||
name = data.get('label', 'Tool')
|
||||
tool_id = data.get('nodeId', 'unknown_tool')
|
||||
instructions = data.get('instructions', '')
|
||||
|
||||
input_connections = self._find_node_inputs(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",
|
||||
]
|
||||
|
||||
# Add instructions if provided
|
||||
if instructions:
|
||||
description.append(f"**Instructions**: {instructions}")
|
||||
|
||||
if input_connections:
|
||||
description.append(f"**Connected to agents**: {', '.join(input_connections)}")
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 { Input } from "@/components/ui/input";
|
||||
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) {
|
||||
return (
|
||||
<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">
|
||||
{workflows.map((workflow) => (
|
||||
<Card key={workflow.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1 flex-1 mr-2">
|
||||
{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>
|
||||
) : (
|
||||
<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
|
||||
key={workflow.id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 group"
|
||||
>
|
||||
{/* Colorful Header */}
|
||||
<div
|
||||
className="h-24 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100"
|
||||
style={{ backgroundColor: getWorkflowColor(workflow.status) }}
|
||||
>
|
||||
<div className="text-3xl text-white drop-shadow-sm">
|
||||
<WorkflowIcon />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="absolute top-3 right-3">
|
||||
{getStatusIcon(workflow.status)}
|
||||
</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">
|
||||
<Link href={`/workflows/builder/${workflow.id}`} className="flex-1">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<Edit className="mr-2 h-3 w-3" />
|
||||
<Edit className="h-3 w-3" />
|
||||
Edit
|
||||
</Button>
|
||||
</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>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
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)}
|
||||
>
|
||||
{deletingWorkflows.has(workflow.id) ? (
|
||||
|
@ -428,7 +450,7 @@ export default function WorkflowsPage() {
|
|||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
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
|
||||
</AlertDialogAction>
|
||||
|
@ -437,8 +459,8 @@ export default function WorkflowsPage() {
|
|||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -213,15 +213,15 @@ export default function NodePalette() {
|
|||
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="input">
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
<Play className="h-4 w-4" />
|
||||
Input
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agent">
|
||||
<Bot className="h-4 w-4 mr-2" />
|
||||
<Bot className="h-4 w-4" />
|
||||
Agents
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools">
|
||||
<Wrench className="h-4 w-4 mr-2" />
|
||||
<Wrench className="h-4 w-4" />
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
@ -257,8 +257,8 @@ export default function NodePalette() {
|
|||
variables: {}
|
||||
}}
|
||||
>
|
||||
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move border-primary/30 bg-primary/5">
|
||||
<CardHeader className="p-4 py-0">
|
||||
<Card className="py-2 group transition-all duration-200 border hover:border-primary/50 cursor-move border-primary/30 bg-primary/5">
|
||||
<CardContent className="p-2 py-0">
|
||||
<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">
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
|
@ -268,12 +268,12 @@ export default function NodePalette() {
|
|||
{node.name}
|
||||
<Badge variant="outline" className="text-xs">Required</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1 line-clamp-2">
|
||||
<CardDescription className="text-xs line-clamp-2">
|
||||
{node.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DraggableNode>
|
||||
);
|
||||
|
@ -310,9 +310,9 @@ export default function NodePalette() {
|
|||
config: {},
|
||||
}}
|
||||
>
|
||||
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move">
|
||||
<CardHeader className="p-4 py-0">
|
||||
<div className="flex items-start gap-3">
|
||||
<Card className="py-2 group transition-all duration-200 border hover:border-primary/50 cursor-move">
|
||||
<CardContent className="p-2 py-0">
|
||||
<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">
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
|
@ -320,12 +320,12 @@ export default function NodePalette() {
|
|||
<CardTitle className="text-sm font-semibold leading-tight">
|
||||
{node.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1 line-clamp-2">
|
||||
<CardDescription className="text-xs line-clamp-2">
|
||||
{node.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DraggableNode>
|
||||
);
|
||||
|
@ -361,10 +361,11 @@ export default function NodePalette() {
|
|||
label: node.name,
|
||||
nodeId: node.id,
|
||||
config: {},
|
||||
instructions: "",
|
||||
}}
|
||||
>
|
||||
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move">
|
||||
<CardHeader className="p-4 py-0">
|
||||
<Card className="py-2 group transition-all duration-200 border hover:border-primary/50 cursor-move">
|
||||
<CardContent className="p-2 py-0">
|
||||
<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">
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
|
@ -378,7 +379,7 @@ export default function NodePalette() {
|
|||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DraggableNode>
|
||||
);
|
||||
|
|
|
@ -84,6 +84,7 @@ const initialNodes: Node[] = [
|
|||
data: {
|
||||
label: 'Web Search',
|
||||
nodeId: 'web_search_tool',
|
||||
instructions: '',
|
||||
outputConnections: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useState } from "react";
|
||||
import { Handle, Position, NodeProps } from "@xyflow/react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
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 {
|
||||
Database,
|
||||
FileText,
|
||||
|
@ -14,18 +19,24 @@ import {
|
|||
Cloud,
|
||||
Send,
|
||||
Settings,
|
||||
Wrench
|
||||
Wrench,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from "lucide-react";
|
||||
import { useWorkflow } from "../WorkflowContext";
|
||||
|
||||
interface ToolConnectionNodeData {
|
||||
label: string;
|
||||
nodeId?: string;
|
||||
toolType?: string;
|
||||
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 [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
const { updateNodeData } = useWorkflow();
|
||||
|
||||
const getToolConfig = () => {
|
||||
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>
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
|
|
Loading…
Reference in New Issue