Elise UI

Loading Stacked

One row for each step of the run: tool calls, messages and thinking. Rows change to done or failed, and can shrink into one line at the end. Press Play to send fake events.

<AGUILoadingStacked />
The stacked loader, built from parts. Write one row; it repeats for each step. The parts have no colors: style rows with data-status and data-kind.
idle
import {
  CircleAlert,
  CircleCheck,
  Inbox,
  Search,
  type LucideIcon,
} from "lucide-react";

import {
  AGUILoadingStacked,
  AGUILoadingStackedEarlier,
  AGUILoadingStackedStepIcon,
  AGUILoadingStackedSteps,
  AGUILoadingStackedStepText,
  AGUILoadingStackedSummaryDone,
  AGUILoadingStackedSummaryFailed,
} from "@/core/agui-loading/adapters/react";
import {
  aguiLoadingStackedReducer,
  EventType,
  initialAGUILoadingStackedState,
} from "@/core/agui-loading";

import { useAGUILoadingDocsReplay } from "../../hooks";
import { AGUILoadingDocsCard } from "../agui-loading-docs-card";
import source from "./agui-loading-docs-loading-stacked-example.tsx?raw";

// An icon for each tool, found by the tool's name.
const TOOL_ICONS: Record<string, LucideIcon> = {
  listCommunications: Inbox,
  searchChannels: Search,
};

export function AGUILoadingDocsLoadingStackedExample() {
  // -----
  // Step 1: keep track of what the agent is doing.
  //
  // In your app, write:
  //   const [state, dispatch] = useReducer(
  //     aguiLoadingStackedReducer,
  //     initialAGUILoadingStackedState,
  //   );
  // then call dispatch(event) for each AG-UI event you
  // receive. Here the docs send fake events for you
  // when you press "Play".
  // -----
  const { state, play, reset } = useAGUILoadingDocsReplay(
    aguiLoadingStackedReducer,
    initialAGUILoadingStackedState,
  );

  return (
    <AGUILoadingDocsCard
      title="<AGUILoadingStacked />"
      description="The stacked loader, built from parts. Write one row; it repeats for each step. The parts have no colors: style rows with data-status and data-kind."
      status={state.lastEventType ?? state.status}
      onPlay={() => play("two-tool-calls")}
      onPlayError={() => play("mid-stream-error")}
      onReset={reset}
      source={source}
    >
      {/* Step 2: build the loader from parts. Put them in any order. */}
      <AGUILoadingStacked
        // Required. The state from step 1.
        state={state}
        // Show only the 4 newest rows.
        maxVisible={4}
        // When the agent finishes:
        //   "keep"     the rows stay (default)
        //   "collapse" the rows shrink into one line
        //   "hide"     everything goes away
        // whenDone="collapse"
        // Wait this many milliseconds before "collapse" or "hide".
        // lingerMs={1500}
        className="text-sm text-muted-foreground"
      >
        {/* Shows "+2 earlier" when some rows don't fit. */}
        <AGUILoadingStackedEarlier className="pl-5 text-xs text-foreground/40" />

        {/* -----
            One row. Write it one time; it repeats for each step.
            Each row has two attributes you can style with:
              data-status: active, done or failed
              data-kind:   run, tool, message, reasoning or error
            ----- */}
        <AGUILoadingStackedSteps rowClassName="group/row data-[status=done]:text-foreground/60 data-[status=failed]:text-destructive">
          {/* -----
              The icon. Pick one per event that started the row.
              busyByEvent: while the row works
              doneByEvent / failedByEvent: when it ends
              Rows you don't list show a spinner, a check or "!".
              ----- */}
          <AGUILoadingStackedStepIcon
            className="group-data-[status=done]/row:text-emerald-500"
            busyByEvent={{
              // A function gets the row, so each tool can have its own icon.
              [EventType.TOOL_CALL_START]: (step) => {
                const ToolIcon = TOOL_ICONS[step.toolCallName ?? ""];
                return ToolIcon ? (
                  <ToolIcon className="size-3 animate-pulse" />
                ) : undefined;
              },
            }}
          />

          {/* -----
              The text. Same keys as the icon.
              busyByEvent: phrases that change while the row works
              doneByEvent: the text when the row is finished
              Rows you don't list show the built-in text.
              ----- */}
          <AGUILoadingStackedStepText
            busyByEvent={{
              [EventType.TOOL_CALL_START]: (step) => [
                `Calling ${step.toolCallName}...`,
              ],
              [EventType.TEXT_MESSAGE_START]: ["Writing your answer..."],
            }}
            doneByEvent={{
              [EventType.RUN_STARTED]: "Connected",
              [EventType.TOOL_CALL_START]: (step) =>
                `Checked ${step.toolCallName}`,
              [EventType.TEXT_MESSAGE_START]: "Answer ready",
            }}
          />

          {/* Your own element. This one shows "tool" on tool rows only. */}
          <span className="hidden text-xs text-foreground/40 group-data-[kind=tool]/row:inline">
            tool
          </span>
        </AGUILoadingStackedSteps>

        {/* -----
            The one line after the rows collapse.
            SummaryDone shows when the run finished.
            SummaryFailed shows when the run failed.
            text: your words. A function gets the number of steps.
            children: an icon before the text.
            ----- */}
        <AGUILoadingStackedSummaryDone
          text={(steps) => `Done · ${steps} steps`}
        >
          <CircleCheck className="size-3 text-emerald-500" />
        </AGUILoadingStackedSummaryDone>

        <AGUILoadingStackedSummaryFailed
          text={(steps) => `Stopped after ${steps} steps`}
        >
          <CircleAlert className="size-3 text-destructive" />
        </AGUILoadingStackedSummaryFailed>
      </AGUILoadingStacked>
    </AGUILoadingDocsCard>
  );
}