File size: 4,403 Bytes
3193174
 
81f5c1c
3193174
81f5c1c
3193174
 
 
 
 
 
 
 
 
 
 
 
 
 
81f5c1c
3193174
 
 
 
81f5c1c
3193174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81f5c1c
 
 
 
 
 
 
 
 
 
3193174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { memo } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { Bot, Play, CheckCircle2, XCircle, Clock, Cpu, Wrench, Pencil } from "lucide-react";
import { cn } from "@/lib/utils";
import { useGraphStore } from "@/stores/graphStore";
import type { AgentNodeData } from "@/stores/graphStore";

const statusConfig = {
  idle: { color: "border-border", icon: null, bg: "bg-card" },
  pending: { color: "border-yellow-400", icon: Clock, bg: "bg-yellow-50 dark:bg-yellow-950" },
  running: { color: "border-blue-500", icon: Play, bg: "bg-blue-50 dark:bg-blue-950" },
  completed: { color: "border-green-500", icon: CheckCircle2, bg: "bg-green-50 dark:bg-green-950" },
  error: { color: "border-red-500", icon: XCircle, bg: "bg-red-50 dark:bg-red-950" },
};

function AgentNodeComponent({ data }: NodeProps) {
  const nodeData = data as unknown as AgentNodeData;
  const status = statusConfig[nodeData.executionStatus] || statusConfig.idle;
  const StatusIcon = status.icon;
  const setEditingNodeId = useGraphStore((s) => s.setEditingNodeId);

  return (
    <div
      className={cn(
        "group min-w-[200px] rounded-lg border-2 shadow-sm transition-all",
        status.color,
        status.bg,
        nodeData.isStartNode && "ring-2 ring-green-400 ring-offset-2",
        nodeData.isEndNode && "ring-2 ring-orange-400 ring-offset-2"
      )}
    >
      <Handle type="target" position={Position.Top} className="!bg-primary !w-3 !h-3" />

      <div className="p-3">
        <div className="flex items-center gap-2 mb-1">
          {StatusIcon ? (
            <StatusIcon
              className={cn(
                "h-4 w-4",
                nodeData.executionStatus === "running" && "animate-pulse text-blue-500",
                nodeData.executionStatus === "completed" && "text-green-500",
                nodeData.executionStatus === "error" && "text-red-500",
                nodeData.executionStatus === "pending" && "text-yellow-500"
              )}
            />
          ) : (
            <Bot className="h-4 w-4 text-primary" />
          )}
          <span className="font-medium text-sm truncate flex-1">{nodeData.displayName}</span>
          <button
            className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-accent"
            onClick={(e) => {
              e.stopPropagation();
              setEditingNodeId(nodeData.agentId);
            }}
          >
            <Pencil className="h-3 w-3 text-muted-foreground" />
          </button>
        </div>

        {nodeData.persona && (
          <p className="text-xs text-muted-foreground line-clamp-2 mb-2">{nodeData.persona}</p>
        )}

        <div className="flex flex-wrap gap-1">
          {nodeData.llmBackbone && (
            <span className="inline-flex items-center gap-0.5 rounded bg-secondary px-1.5 py-0.5 text-[10px]">
              <Cpu className="h-2.5 w-2.5" />
              {nodeData.llmBackbone}
            </span>
          )}
          {nodeData.tools.slice(0, 2).map((tool) => (
            <span
              key={tool}
              className="inline-flex items-center gap-0.5 rounded bg-secondary px-1.5 py-0.5 text-[10px]"
            >
              <Wrench className="h-2.5 w-2.5" />
              {tool}
            </span>
          ))}
          {nodeData.tools.length > 2 && (
            <span className="rounded bg-secondary px-1.5 py-0.5 text-[10px]">
              +{nodeData.tools.length - 2}
            </span>
          )}
        </div>

        {nodeData.tokensUsed !== undefined && (
          <div className="mt-1 text-[10px] text-muted-foreground">
            {nodeData.tokensUsed} tokens
          </div>
        )}

        <div className="flex gap-1 mt-1">
          {nodeData.isStartNode && (
            <span className="rounded bg-green-100 dark:bg-green-900 px-1 py-0.5 text-[10px] text-green-700 dark:text-green-300">
              START
            </span>
          )}
          {nodeData.isEndNode && (
            <span className="rounded bg-orange-100 dark:bg-orange-900 px-1 py-0.5 text-[10px] text-orange-700 dark:text-orange-300">
              END
            </span>
          )}
        </div>
      </div>

      <Handle type="source" position={Position.Bottom} className="!bg-primary !w-3 !h-3" />
    </div>
  );
}

export const AgentNode = memo(AgentNodeComponent);