-

Workflow Node Card

PreviousNext

A workflow node summary card for LLM, retrieval, condition, tool, and approval steps.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/workflow-node-card.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/workflow-node-card.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/workflow-node-card.json

Component

components/ui/workflow-node-card.tsx
"use client"

import * as React from "react"
import {
  AlertCircleIcon,
  CheckCircle2Icon,
  CircleIcon,
  Clock3Icon,
  Loader2Icon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"

export type WorkflowNodeStatus =
  | "idle"
  | "running"
  | "success"
  | "error"
  | "waiting"

export interface WorkflowNodePort {
  label: React.ReactNode
  type?: React.ReactNode
}

export interface WorkflowNodeCardProps
  extends Omit<React.ComponentProps<"div">, "title"> {
  icon?: React.ReactNode
  title: React.ReactNode
  type?: React.ReactNode
  description?: React.ReactNode
  status?: WorkflowNodeStatus
  inputs?: WorkflowNodePort[]
  outputs?: WorkflowNodePort[]
  footer?: React.ReactNode
}

const statusConfig: Record<
  WorkflowNodeStatus,
  {
    label: string
    icon: React.ComponentType<{ className?: string }>
    className: string
  }
> = {
  idle: {
    label: "Idle",
    icon: CircleIcon,
    className: "border-border bg-muted text-muted-foreground",
  },
  running: {
    label: "Running",
    icon: Loader2Icon,
    className:
      "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300",
  },
  success: {
    label: "Success",
    icon: CheckCircle2Icon,
    className:
      "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
  },
  error: {
    label: "Error",
    icon: AlertCircleIcon,
    className: "border-destructive/20 bg-destructive/10 text-destructive",
  },
  waiting: {
    label: "Waiting",
    icon: Clock3Icon,
    className:
      "border-amber-500/25 bg-amber-500/10 text-amber-700 dark:text-amber-300",
  },
}

function PortList({
  label,
  ports = [],
}: {
  label: string
  ports?: WorkflowNodePort[]
}) {
  if (!ports.length) return null

  return (
    <div className="space-y-1.5">
      <p className="text-muted-foreground text-[11px] font-medium tracking-normal uppercase">
        {label}
      </p>
      <div className="space-y-1">
        {ports.map((port, index) => (
          <div
            key={index}
            className="border-border bg-muted/40 flex min-h-7 items-center justify-between gap-2 rounded-md border px-2 text-xs"
          >
            <span className="truncate">{port.label}</span>
            {port.type ? (
              <code className="text-muted-foreground shrink-0 font-mono text-[11px]">
                {port.type}
              </code>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  )
}

export function WorkflowNodeCard({
  icon,
  title,
  type,
  description,
  status = "idle",
  inputs,
  outputs,
  footer,
  className,
  ...props
}: WorkflowNodeCardProps) {
  const config = statusConfig[status]
  const StatusIcon = config.icon

  return (
    <div
      className={cn(
        "border-border bg-card text-card-foreground w-full overflow-hidden rounded-lg border shadow-sm",
        className
      )}
      {...props}
    >
      <div className="border-border flex items-start gap-3 border-b p-3">
        {icon ? (
          <div className="bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-md">
            {icon}
          </div>
        ) : null}
        <div className="min-w-0 flex-1">
          <div className="flex min-w-0 items-center gap-2">
            <p className="truncate text-sm font-medium">{title}</p>
            {type ? (
              <Badge variant="secondary" className="h-6 rounded-md px-2">
                {type}
              </Badge>
            ) : null}
          </div>
          {description ? (
            <p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
              {description}
            </p>
          ) : null}
        </div>
        <Badge
          variant="outline"
          className={cn("h-6 gap-1 rounded-md px-2", config.className)}
        >
          <StatusIcon
            className={cn("size-3", status === "running" && "animate-spin")}
          />
          {config.label}
        </Badge>
      </div>
      <div className="grid gap-3 p-3 sm:grid-cols-2">
        <PortList label="Inputs" ports={inputs} />
        <PortList label="Outputs" ports={outputs} />
      </div>
      {footer ? (
        <div className="border-border bg-muted/20 border-t px-3 py-2 text-xs">
          {footer}
        </div>
      ) : null}
    </div>
  )
}

Usage

import { BrainCircuitIcon } from "lucide-react"
 
import { WorkflowNodeCard } from "@/components/ui/workflow-node-card"
 
export function Example() {
  return (
    <WorkflowNodeCard
      icon={<BrainCircuitIcon className="size-5" />}
      title="Classify intent"
      type="LLM"
      status="running"
      inputs={[{ label: "conversation.message", type: "string" }]}
      outputs={[{ label: "intent", type: "enum" }]}
    />
  )
}

Props

PropTypeDefault
titleReactNoderequired
iconReactNode-
typeReactNode-
descriptionReactNode-
statusWorkflowNodeStatus"idle"
inputsWorkflowNodePort[][]
outputsWorkflowNodePort[][]
footerReactNode-