← All proposals

Thinking State

A small inline status: the agent icon (Zappy by default), a label ("Thinking..."), and the elapsed time in the code font ("17.1s").

Net-new componentOpen in Storybook

proposals/ThinkingState/ThinkingState.tsx99 lines · View on GitHub
import { ComponentPropsWithoutRef } from 'react';

import { Icon, IconProps } from '../Icon/Icon';

import './thinking-state.css';

export type ThinkingStateSize = 'xs' | 's' | 'm';

export type ThinkingStateState = 'active' | 'completed' | 'stopped';

export interface ThinkingStateProps extends ComponentPropsWithoutRef<'div'> {
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * Which icon to render before the label.
	 */
	iconName?: IconProps['iconName'];
	/**
	 * What the agent is doing (for example "Thinking..."). Announced to screen
	 * readers.
	 */
	label: string;
	/**
	 * Size. Sets the text size, and the icon size follows it (the icon is the
	 * same height as the text); the time is one step smaller and the gap grows
	 * with the size.
	 */
	size?: ThinkingStateSize;
	/**
	 * Where the run is. `active` sweeps a shimmer across the label (the PDS
	 * `pds-ai-active-shimmer` utility) and pulses the icon. `completed` shows
	 * the same text without motion. `stopped` is for a run the user cut off:
	 * no motion, and the icon and label are muted to show it did not finish.
	 */
	state?: ThinkingStateState;
	/**
	 * Should the elapsed time be shown? It only renders when `time` is also set.
	 */
	showTimer?: boolean;
	/**
	 * Elapsed time, formatted by the consumer (for example "17.1s"). Hidden from
	 * screen readers so a ticking value isn't announced.
	 */
	time?: string;
}

// Stand-in for the v2 `mergeClasses` util, which the published package does
// not export. The port uses the real one.
const mergeClasses = (classes: Array<string | null | undefined>) =>
	classes.filter(Boolean).join(' ');

/**
 * ThinkingState UI component. Shows that an agent is working: an icon, what it
 * is doing, and how long it has been at it.
 */
export const ThinkingState = ({
	className,
	iconName = 'zappy',
	label,
	showTimer = false,
	size = 's',
	state = 'active',
	time,
	...props
}: ThinkingStateProps) => {
	const baseClass = 'pds-thinking-state';
	const isActive = state === 'active';

	return (
		<div
			className={mergeClasses([
				baseClass,
				size !== 's' ? `${baseClass}--${size}` : null,
				`${baseClass}--${state}`,
				className,
			])}
			role='status'
			{...props}
		>
			<Icon className={`${baseClass}__icon`} iconName={iconName} size={size} />
			<span
				className={mergeClasses([
					`${baseClass}__label`,
					isActive ? 'pds-ai-active-shimmer' : null,
				])}
			>
				{label}
			</span>
			{showTimer && time && (
				<span aria-hidden='true' className={`${baseClass}__time`}>
					{time}
				</span>
			)}
		</div>
	);
};