/** * The three states every artifact has and no model should have to draw: * loading, empty, and error. * * The skill mandates rendering all three ("always render the loading and error * states from useQuery"), and until now gave the model nothing to render them * WITH — so each artifact invented its own spinner, its own "no data" text, and, * when something threw, the shell showed a raw stack trace in a `
`. Three
 * artifacts side by side looked like three products.
 *
 * These are built from the same Skeleton / Alert primitives the console uses, in
 * `design.md`'s idiom: hairline borders, grayscale, mono for metadata, sentence
 * case, and errors that say what happened plus what to do. They are bound into
 * generated-code scope through `components.ts`, so `ArtifactLoading` is simply
 * in scope the way `Card` is.
 */

import type { ReactNode } from "react";

import { Alert, AlertDescription, AlertTitle } from "@executor-js/react/components/alert";
import { Button } from "@executor-js/react/components/button";
import { Skeleton } from "@executor-js/react/components/skeleton";
import { cn } from "@executor-js/react/lib/utils";
import { AlertCircle } from "lucide-react";

/**
 * The waiting state, shaped like the thing being waited for.
 *
 * A spinner says "something is happening"; a skeleton in the layout's own shape
 * says "this is what is arriving", and the page does not jump when it does. So
 * the variant is the layout, not a size.
 */
export function ArtifactLoading({
  variant = "list",
  rows = 4,
  label,
  className,
}: {
  /** `list` for stacked rows, `table` for a header plus rows, `cards` for a
   *  metric row, `chart` for a plot area. */
  readonly variant?: "list" | "table" | "cards" | "chart";
  /** How many rows or cards to suggest. Ignored by `chart`. */
  readonly rows?: number;
  /** Optional mono caption, e.g. `Loading deployments...`. */
  readonly label?: string;
  readonly className?: string;
}) {
  const count = Math.max(1, Math.min(rows, 12));
  const items = Array.from({ length: count }, (_, index) => index);

  return (
    
{label && (
{label}
)} {variant === "cards" && (
{items.slice(0, 4).map((index) => (
))}
)} {variant === "chart" && (
)} {variant === "table" && (
{items.map((index) => (
))}
)} {variant === "list" && (
{items.map((index) => ( ))}
)}
); } /** * Nothing to show, which is not an error. * * Per `design.md`, an empty state points at the first action rather than * announcing absence, so `action` is where a button goes when one exists. No * illustration and no icon: type and a hairline carry it. */ export function ArtifactEmpty({ title = "Nothing to show", description, action, className, }: { /** Sentence case, specific: `No deployments in the last 7 days`. */ readonly title?: string; /** What would put something here. */ readonly description?: ReactNode; /** The first action, usually a `Button`. */ readonly action?: ReactNode; readonly className?: string; }) { return (
{title}
{description && (
{description}
)} {action &&
{action}
}
); } /** The message out of whatever a query, a mutation or an error boundary handed * us — `Error`, a string, or one of the shapes a tool result can fail with. */ const messageOf = (error: unknown): string => { if (typeof error === "string") return error; if (error instanceof Error) return error.message; if (typeof error === "object" && error !== null) { const record = error as { message?: unknown; error?: { message?: unknown } }; if (typeof record.message === "string") return record.message; if (typeof record.error?.message === "string") return record.error.message; } return "Unknown error"; }; /** * Something failed. * * Deliberately NOT a stack trace: a stack is for whoever wrote the code, and * the person looking at an artifact did not. They get what happened plus what * to do — `design.md`'s error contract — with `onRetry` when retrying is the * answer. The underlying message is shown, because "something went wrong" is * unactionable, but it is set in mono at metadata size rather than dominating * the frame. */ export function ArtifactError({ error, title = "Couldn't load this", hint, onRetry, className, }: { /** The thrown value, or `query.error`. */ readonly error: unknown; /** Sentence case, what failed. */ readonly title?: string; /** What to do next. Defaults to a retry hint when `onRetry` is given. */ readonly hint?: ReactNode; readonly onRetry?: () => void; readonly className?: string; }) { const message = messageOf(error); const fallbackHint = onRetry ? "Retry, or check the integration is still connected." : undefined; return ( {title} {message} {(hint ?? fallbackHint) && ( {hint ?? fallbackHint} )} {onRetry && ( )} ); }