/** * 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 && (
)}
);
}