# AmroUI agents

Installable AmroUI product component source.

## Installation

```bash
npx shadcn@latest add https://amro-ui.vercel.app/r/amro-ui-agents.json
```

[Registry JSON](https://amro-ui.vercel.app/r/amro-ui-agents.json)

## Source

### source/components/agents.tsx

```tsx
import {
  CheckCircle2,
  Clock3,
  FileImage,
  Link2,
  LockKeyhole,
  Paperclip,
  ShieldCheck,
  Sparkles,
  Upload,
  UserRoundCheck,
  Wrench
} from "lucide-react";
import {
  forwardRef,
  useId,
  useState,
  type FormEvent,
  type HTMLAttributes,
  type ReactNode
} from "react";
import { cn } from "../lib/cn.js";
import { Avatar } from "./avatar.js";
import { Badge } from "./badge.js";
import { Button } from "./button.js";
import {
  EmptyNotice,
  FeaturePanel,
  MetricGrid,
  StatusIndicator,
  type AmroProcessStatus,
  type MetricItem
} from "./feature-primitives.js";
import { Input, Select, Textarea } from "./input.js";

export type IssueSeverity = "low" | "medium" | "high" | "critical";

export interface IssueReportValue {
  category: string;
  severity: IssueSeverity;
  summary: string;
  details: string;
  includeDiagnostics: boolean;
}

export interface IssueReportComposerProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onSubmit" | "defaultValue"
> {
  defaultValue?: Partial<IssueReportValue>;
  categories?: readonly string[];
  screenshotName?: string;
  onAttachScreenshot?: () => void;
  onSubmit?: (value: IssueReportValue) => void;
}

const severityOptions: readonly IssueSeverity[] = ["low", "medium", "high", "critical"];

export const IssueReportComposer = forwardRef<HTMLFormElement, IssueReportComposerProps>(
  (
    {
      categories = ["Unexpected result", "Data issue", "Integration", "Account access"],
      className,
      defaultValue,
      onAttachScreenshot,
      onSubmit,
      screenshotName,
      ...props
    },
    ref
  ) => {
    const detailsId = useId();
    const [category, setCategory] = useState(defaultValue?.category ?? categories[0] ?? "Other");
    const [severity, setSeverity] = useState<IssueSeverity>(defaultValue?.severity ?? "medium");
    const [summary, setSummary] = useState(defaultValue?.summary ?? "");
    const [details, setDetails] = useState(defaultValue?.details ?? "");
    const [includeDiagnostics, setIncludeDiagnostics] = useState(
      defaultValue?.includeDiagnostics ?? true
    );
    const submit = (event: FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      onSubmit?.({ category, severity, summary, details, includeDiagnostics });
    };

    return (
      <form ref={ref} className={cn("amro-issue-composer", className)} onSubmit={submit} {...props}>
        <header>
          <div>
            <span className="amro-eyebrow">Report an issue</span>
            <strong>Help the team investigate</strong>
          </div>
          <StatusIndicator label="Draft" status="pending" />
        </header>
        <div className="amro-form-grid">
          <label>
            <span>Category</span>
            <Select value={category} onChange={(event) => setCategory(event.currentTarget.value)}>
              {categories.map((option) => (
                <option key={option}>{option}</option>
              ))}
            </Select>
          </label>
          <fieldset>
            <legend>Severity</legend>
            <div className="amro-segmented-field">
              {severityOptions.map((option) => (
                <button
                  aria-pressed={severity === option}
                  key={option}
                  onClick={() => setSeverity(option)}
                  type="button"
                >
                  {option}
                </button>
              ))}
            </div>
          </fieldset>
        </div>
        <Input
          label="Short summary"
          onChange={(event) => setSummary(event.currentTarget.value)}
          placeholder="What went wrong?"
          required
          value={summary}
        />
        <label htmlFor={detailsId}>
          <span>What happened?</span>
          <Textarea
            id={detailsId}
            onChange={(event) => setDetails(event.currentTarget.value)}
            placeholder="Include the expected outcome and the steps that led here."
            required
            rows={5}
            value={details}
          />
        </label>
        <div className="amro-attachment-row">
          <button onClick={onAttachScreenshot} type="button">
            {screenshotName ? <FileImage aria-hidden="true" /> : <Upload aria-hidden="true" />}
            <span>
              <strong>{screenshotName ?? "Attach screenshot"}</strong>
              <small>{screenshotName ? "Screenshot ready" : "PNG, JPG, or WebP"}</small>
            </span>
          </button>
          <label className="amro-check-row">
            <input
              checked={includeDiagnostics}
              onChange={(event) => setIncludeDiagnostics(event.currentTarget.checked)}
              type="checkbox"
            />
            <span>
              <strong>Include diagnostics</strong>
              <small>Browser, page, and recent request identifiers</small>
            </span>
          </label>
        </div>
        <footer>
          <span>
            <LockKeyhole aria-hidden="true" /> Sensitive values are redacted automatically.
          </span>
          <Button disabled={!summary.trim() || !details.trim()} type="submit">
            Send report
          </Button>
        </footer>
      </form>
    );
  }
);
IssueReportComposer.displayName = "IssueReportComposer";

export interface PoweredByFooterProps extends HTMLAttributes<HTMLElement> {
  productName?: string;
  href?: string;
  showPrivacy?: boolean;
  onPrivacyClick?: () => void;
}

export const PoweredByFooter = forwardRef<HTMLElement, PoweredByFooterProps>(
  (
    {
      className,
      href = "https://amro-ui.vercel.app",
      onPrivacyClick,
      productName = "AmroUI",
      showPrivacy = true,
      ...props
    },
    ref
  ) => (
    <footer ref={ref} className={cn("amro-powered-footer", className)} {...props}>
      <a href={href} rel="noreferrer" target="_blank">
        <Sparkles aria-hidden="true" /> Powered by <strong>{productName}</strong>
      </a>
      {showPrivacy && (
        <button onClick={onPrivacyClick} type="button">
          Privacy
        </button>
      )}
    </footer>
  )
);
PoweredByFooter.displayName = "PoweredByFooter";

export type AgentPresence = "idle" | "working" | "waiting" | "complete" | "error";

export interface AgentStatus {
  id: string;
  name: string;
  role?: string;
  task: string;
  presence: AgentPresence;
  progress?: number;
  avatarUrl?: string;
}

export interface AgentStatusRowProps extends HTMLAttributes<HTMLDivElement> {
  agent?: AgentStatus;
  onOpen?: (agent: AgentStatus) => void;
}

const defaultAgent: AgentStatus = {
  id: "research",
  name: "Research agent",
  role: "Specialist",
  task: "Verifying three source claims",
  presence: "working",
  progress: 68
};

const agentStatusMap: Record<AgentPresence, AmroProcessStatus> = {
  idle: "pending",
  working: "active",
  waiting: "blocked",
  complete: "complete",
  error: "error"
};

export const AgentStatusRow = forwardRef<HTMLDivElement, AgentStatusRowProps>(
  ({ agent = defaultAgent, className, onOpen, ...props }, ref) => {
    const progress = Math.max(0, Math.min(100, agent.progress ?? 0));
    return (
      <div ref={ref} className={cn("amro-agent-row", className)} {...props}>
        <Avatar name={agent.name} {...(agent.avatarUrl ? { src: agent.avatarUrl } : {})} />
        <span className="amro-agent-row__copy">
          <span>
            <strong>{agent.name}</strong>
            {agent.role && <small>{agent.role}</small>}
          </span>
          <span>{agent.task}</span>
          {agent.progress !== undefined && (
            <span
              className="amro-agent-row__progress"
              role="progressbar"
              aria-label={`${agent.name} progress`}
              aria-valuemin={0}
              aria-valuemax={100}
              aria-valuenow={progress}
            >
              <i style={{ width: `${progress}%` }} />
            </span>
          )}
        </span>
        <StatusIndicator label={agent.presence} status={agentStatusMap[agent.presence]} />
        {onOpen && (
          <button aria-label={`Open ${agent.name}`} onClick={() => onOpen(agent)} type="button">
            Open
          </button>
        )}
      </div>
    );
  }
);
AgentStatusRow.displayName = "AgentStatusRow";

const defaultAgentTeam: readonly AgentStatus[] = [
  {
    id: "sales",
    name: "Sales agent",
    task: "Scoring account fit",
    presence: "working",
    progress: 74
  },
  { id: "support", name: "Support agent", task: "Standing by", presence: "idle" },
  {
    id: "operations",
    name: "Operations agent",
    task: "Workflow completed",
    presence: "complete",
    progress: 100
  }
];

export interface AgentTeamPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  agents?: readonly AgentStatus[];
  title?: ReactNode;
  onAgentOpen?: (agent: AgentStatus) => void;
}

export const AgentTeamPanel = forwardRef<HTMLDivElement, AgentTeamPanelProps>(
  (
    { agents = defaultAgentTeam, className, onAgentOpen, title = "Specialist team", ...props },
    ref
  ) => {
    const active = agents.filter((agent) => agent.presence === "working").length;
    return (
      <FeaturePanel
        ref={ref}
        className={cn("amro-agent-team", className)}
        eyebrow="Coordination"
        title={title}
        status={<StatusIndicator label={`${active} active`} status={active ? "active" : "idle"} />}
        {...props}
      >
        {agents.length ? (
          <div className="amro-agent-team__list">
            {agents.map((agent) => (
              <AgentStatusRow
                agent={agent}
                key={agent.id}
                {...(onAgentOpen ? { onOpen: onAgentOpen } : {})}
              />
            ))}
          </div>
        ) : (
          <EmptyNotice
            title="No agents assigned"
            description="Add a specialist to begin this workflow."
          />
        )}
      </FeaturePanel>
    );
  }
);
AgentTeamPanel.displayName = "AgentTeamPanel";

const defaultOutcomeMetrics: readonly MetricItem[] = [
  { id: "tasks", label: "Tasks completed", value: "128", detail: "+18 this week" },
  { id: "hours", label: "Hours saved", value: "42.6", detail: "Verified estimate" },
  { id: "runs", label: "Workflow runs", value: "31", detail: "96.8% success" }
];

export interface AgentOutcomeMetricsProps extends HTMLAttributes<HTMLDListElement> {
  metrics?: readonly MetricItem[];
  emptyLabel?: ReactNode;
}

export const AgentOutcomeMetrics = forwardRef<HTMLDListElement, AgentOutcomeMetricsProps>(
  (
    {
      className,
      emptyLabel = "Outcomes appear after the first completed workflow.",
      metrics = defaultOutcomeMetrics,
      ...props
    },
    ref
  ) => (
    <MetricGrid
      ref={ref}
      className={cn("amro-agent-outcomes", className)}
      emptyLabel={emptyLabel}
      items={metrics}
      {...props}
    />
  )
);
AgentOutcomeMetrics.displayName = "AgentOutcomeMetrics";

export interface LiveOutcomeCardProps extends HTMLAttributes<HTMLDivElement> {
  outcome?: ReactNode;
  detail?: ReactNode;
  timestamp?: ReactNode;
  status?: "live" | "verified" | "pending";
  onOpen?: () => void;
}

export const LiveOutcomeCard = forwardRef<HTMLDivElement, LiveOutcomeCardProps>(
  (
    {
      className,
      detail = "Demo booked with Northstar Labs · Tuesday at 14:30",
      onOpen,
      outcome = "Qualified meeting booked",
      status = "verified",
      timestamp = "Just now",
      ...props
    },
    ref
  ) => (
    <div ref={ref} className={cn("amro-live-outcome", className)} {...props}>
      <span className="amro-live-outcome__icon">
        <CheckCircle2 aria-hidden="true" />
      </span>
      <span>
        <span className="amro-eyebrow">Real-world outcome</span>
        <strong>{outcome}</strong>
        <small>{detail}</small>
      </span>
      <span>
        <Badge dot tone={status === "verified" ? "success" : "info"}>
          {status}
        </Badge>
        <time>{timestamp}</time>
      </span>
      {onOpen && (
        <Button onClick={onOpen} size="sm" variant="secondary">
          View receipt
        </Button>
      )}
    </div>
  )
);
LiveOutcomeCard.displayName = "LiveOutcomeCard";

export interface AgentActivityItem {
  id: string;
  agent: string;
  action: ReactNode;
  detail?: ReactNode;
  timestamp: ReactNode;
  type?: "research" | "tool" | "approval" | "outcome";
}

const defaultActivity: readonly AgentActivityItem[] = [
  {
    id: "one",
    agent: "Research agent",
    action: "Verified account evidence",
    detail: "3 sources approved",
    timestamp: "09:42",
    type: "research"
  },
  {
    id: "two",
    agent: "Sales agent",
    action: "Prepared outreach draft",
    detail: "Waiting for human approval",
    timestamp: "09:39",
    type: "approval"
  },
  {
    id: "three",
    agent: "Meeting agent",
    action: "Created calendar hold",
    timestamp: "09:31",
    type: "outcome"
  }
];

export interface RecentAgentActivityProps extends HTMLAttributes<HTMLDivElement> {
  items?: readonly AgentActivityItem[];
  onItemOpen?: (item: AgentActivityItem) => void;
}

const activityIcon = {
  research: Sparkles,
  tool: Wrench,
  approval: UserRoundCheck,
  outcome: CheckCircle2
};

export const RecentAgentActivity = forwardRef<HTMLDivElement, RecentAgentActivityProps>(
  ({ className, items = defaultActivity, onItemOpen, ...props }, ref) => (
    <div ref={ref} className={cn("amro-agent-activity", className)} {...props}>
      <header>
        <span>
          <span className="amro-eyebrow">Live operations</span>
          <strong>Recent agent activity</strong>
        </span>
        <Badge dot tone="success">
          Live
        </Badge>
      </header>
      {items.length ? (
        <ol>
          {items.map((item) => {
            const Icon = activityIcon[item.type ?? "tool"];
            return (
              <li key={item.id}>
                <span className="amro-agent-activity__icon">
                  <Icon aria-hidden="true" />
                </span>
                <span>
                  <strong>{item.action}</strong>
                  <small>
                    {item.agent}
                    {item.detail !== undefined && <> · {item.detail}</>}
                  </small>
                </span>
                <time>{item.timestamp}</time>
                {onItemOpen && (
                  <button
                    aria-label={`Open ${item.agent} activity`}
                    onClick={() => onItemOpen(item)}
                    type="button"
                  >
                    Open
                  </button>
                )}
              </li>
            );
          })}
        </ol>
      ) : (
        <EmptyNotice title="No recent activity" />
      )}
    </div>
  )
);
RecentAgentActivity.displayName = "RecentAgentActivity";

export interface ControlBoundary {
  id: string;
  label: ReactNode;
  description: ReactNode;
  state: "allowed" | "approval" | "blocked";
  icon?: ReactNode;
}

const defaultBoundaries: readonly ControlBoundary[] = [
  {
    id: "knowledge",
    label: "Your knowledge",
    description: "Approved collections only",
    state: "allowed",
    icon: <ShieldCheck aria-hidden="true" />
  },
  {
    id: "tools",
    label: "Your tools",
    description: "CRM and calendar connected",
    state: "allowed",
    icon: <Wrench aria-hidden="true" />
  },
  {
    id: "control",
    label: "Your control",
    description: "Human approval before external sends",
    state: "approval",
    icon: <UserRoundCheck aria-hidden="true" />
  }
];

export interface ControlBoundaryCardProps extends HTMLAttributes<HTMLDivElement> {
  boundaries?: readonly ControlBoundary[];
  onManage?: () => void;
}

export const ControlBoundaryCard = forwardRef<HTMLDivElement, ControlBoundaryCardProps>(
  ({ boundaries = defaultBoundaries, className, onManage, ...props }, ref) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-control-boundary", className)}
      eyebrow="Trust contract"
      title="Agent control boundaries"
      actions={
        onManage ? (
          <Button onClick={onManage} size="sm" variant="tertiary">
            Manage
          </Button>
        ) : undefined
      }
      {...props}
    >
      <div className="amro-control-boundary__grid">
        {boundaries.map((boundary) => (
          <article data-state={boundary.state} key={boundary.id}>
            <span>{boundary.icon ?? <LockKeyhole aria-hidden="true" />}</span>
            <strong>{boundary.label}</strong>
            <small>{boundary.description}</small>
            <StatusIndicator
              label={boundary.state === "approval" ? "Approval required" : boundary.state}
              status={
                boundary.state === "allowed"
                  ? "complete"
                  : boundary.state === "approval"
                    ? "blocked"
                    : "error"
              }
            />
          </article>
        ))}
      </div>
    </FeaturePanel>
  )
);
ControlBoundaryCard.displayName = "ControlBoundaryCard";

export interface KnowledgeCollection {
  id: string;
  name: ReactNode;
  documentCount: number;
  updatedAt: ReactNode;
  approved: boolean;
  freshness?: "fresh" | "aging" | "stale";
}

const defaultKnowledge: readonly KnowledgeCollection[] = [
  {
    id: "product",
    name: "Product knowledge",
    documentCount: 42,
    updatedAt: "12 minutes ago",
    approved: true,
    freshness: "fresh"
  },
  {
    id: "support",
    name: "Support playbooks",
    documentCount: 18,
    updatedAt: "Yesterday",
    approved: true,
    freshness: "fresh"
  },
  {
    id: "policy",
    name: "Policy archive",
    documentCount: 9,
    updatedAt: "32 days ago",
    approved: false,
    freshness: "stale"
  }
];

export interface TrustedKnowledgeCardProps extends HTMLAttributes<HTMLDivElement> {
  collections?: readonly KnowledgeCollection[];
  onCollectionOpen?: (collection: KnowledgeCollection) => void;
  onAddSource?: () => void;
}

export const TrustedKnowledgeCard = forwardRef<HTMLDivElement, TrustedKnowledgeCardProps>(
  ({ className, collections = defaultKnowledge, onAddSource, onCollectionOpen, ...props }, ref) => (
    <FeaturePanel
      ref={ref}
      className={cn("amro-trusted-knowledge", className)}
      eyebrow="Grounding"
      title="Trusted knowledge"
      status={
        <StatusIndicator
          label={`${collections.filter((item) => item.approved).length} approved`}
          status="complete"
        />
      }
      {...props}
    >
      {collections.length ? (
        <div className="amro-trusted-knowledge__list">
          {collections.map((collection) => (
            <button
              disabled={!onCollectionOpen}
              key={collection.id}
              onClick={() => onCollectionOpen?.(collection)}
              type="button"
            >
              <span className="amro-trusted-knowledge__icon">
                <Link2 aria-hidden="true" />
              </span>
              <span>
                <strong>{collection.name}</strong>
                <small>
                  {collection.documentCount} documents · Updated {collection.updatedAt}
                </small>
              </span>
              <Badge dot tone={collection.approved ? "success" : "neutral"}>
                {collection.approved ? "Approved" : "Review"}
              </Badge>
              <StatusIndicator
                label={collection.freshness ?? "fresh"}
                status={collection.freshness === "stale" ? "warning" : "online"}
              />
            </button>
          ))}
        </div>
      ) : (
        <EmptyNotice
          title="No knowledge connected"
          description="Connect an approved source to ground assistant answers."
        />
      )}
      {onAddSource && (
        <Button onClick={onAddSource} variant="secondary">
          <Paperclip aria-hidden="true" /> Add source
        </Button>
      )}
    </FeaturePanel>
  )
);
TrustedKnowledgeCard.displayName = "TrustedKnowledgeCard";

export interface HumanInLoopIndicatorProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  stage?: ReactNode;
  reviewer?: ReactNode;
  compact?: boolean;
  onReview?: () => void;
}

export const HumanInLoopIndicator = forwardRef<HTMLDivElement, HumanInLoopIndicatorProps>(
  (
    {
      className,
      compact = false,
      description = "The agent has paused before an externally consequential action.",
      onReview,
      reviewer = "Workspace approver",
      stage = "Send outreach",
      title = "Human approval required",
      ...props
    },
    ref
  ) => (
    <div
      ref={ref}
      className={cn("amro-human-loop", compact && "amro-human-loop--compact", className)}
      role="status"
      {...props}
    >
      <span className="amro-human-loop__icon">
        <UserRoundCheck aria-hidden="true" />
      </span>
      <span>
        <span className="amro-eyebrow">Control boundary · {stage}</span>
        <strong>{title}</strong>
        {!compact && <small>{description}</small>}
      </span>
      <span>
        <Badge tone="info">{reviewer}</Badge>
        <span>
          <Clock3 aria-hidden="true" /> Waiting
        </span>
      </span>
      {onReview && (
        <Button onClick={onReview} size="sm">
          Review
        </Button>
      )}
    </div>
  )
);
HumanInLoopIndicator.displayName = "HumanInLoopIndicator";
```



## Usage

Shared product source installed automatically by AmroUI component entries.

