AI Patterns

errors

Console Error Card

A structured card surfacing a browser console error with a code frame, call stack, and navigation.

1/2Console Error

Encountered a script tag while rendering React component. Scripts inside React components are never executed when rendering on the client. Consider using template tag instead.

src/app/layout.tsx (43:9) @ RootLayout
41 >
42 <head>
>43 <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
44 </head>
45 <body className="flex min-h-full flex-col font-sans">
46 <header className="border-b">
Was this helpful?
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { ChevronLeft, ChevronRight, ChevronDown, Copy, ThumbsUp, ThumbsDown, X, ExternalLink } from "lucide-react";

import { cn } from "@/lib/utils";

export interface CodeLine {
  number: number;
  content: string;
  isError?: boolean;
}

export interface CodeFrame {
  file: string;
  lines: CodeLine[];
}

export interface StackFrame {
  name: string;
  context?: string;
  file?: string;
}

export interface ConsoleErrorCardProps {
  type?: "error" | "warning";
  message: string;
  frame?: CodeFrame;
  stack?: StackFrame[];
  current?: number;
  total?: number;
  onPrev?: () => void;
  onNext?: () => void;
  onClose?: () => void;
  onCopy?: () => void;
  onOpenFrame?: (file: string) => void;
  onHelpful?: (value: boolean) => void;
  className?: string;
}

export function ConsoleErrorCard({
  type = "error",
  message,
  frame,
  stack = [],
  current = 1,
  total = 1,
  onPrev,
  onNext,
  onClose,
  onCopy,
  onOpenFrame,
  onHelpful,
  className,
}: ConsoleErrorCardProps) {
  const [stackOpen, setStackOpen] = React.useState(false);
  const [helpfulVote, setHelpfulVote] = React.useState<boolean | null>(null);
  const [copied, setCopied] = React.useState(false);

  function handleCopy() {
    onCopy?.();
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  }

  function handleHelpful(value: boolean) {
    setHelpfulVote(value);
    onHelpful?.(value);
  }

  const isError = type === "error";

  return (
    <div
      className={cn(
        "w-full overflow-hidden rounded-2xl border border-border bg-background text-foreground",
        className
      )}
    >
      {/* Top bar */}
      <div className="flex items-center justify-between border-b border-border px-3.5 py-2.5">
        <div className="flex items-center gap-2">
          {total > 1 && (
            <>
              <button
                type="button"
                onClick={onPrev}
                disabled={current <= 1}
                aria-label="Previous error"
                className="rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
              >
                <ChevronLeft className="size-4" aria-hidden />
              </button>
              <span className="font-mono text-[11px] text-muted-foreground tabular-nums">
                {current}/{total}
              </span>
              <button
                type="button"
                onClick={onNext}
                disabled={current >= total}
                aria-label="Next error"
                className="rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
              >
                <ChevronRight className="size-4" aria-hidden />
              </button>
            </>
          )}
          <span
            className={cn(
              "rounded-md px-2 py-0.5 text-[11px] font-semibold",
              isError
                ? "bg-red-100 text-red-700 dark:bg-red-950/60 dark:text-red-400"
                : "bg-amber-100 text-amber-700 dark:bg-amber-950/60 dark:text-amber-400"
            )}
          >
            Console {isError ? "Error" : "Warning"}
          </span>
        </div>

        <div className="flex items-center gap-1">
          {onCopy && (
            <button
              type="button"
              onClick={handleCopy}
              aria-label="Copy error"
              className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            >
              <AnimatePresence mode="wait" initial={false}>
                {copied ? (
                  <motion.svg
                    key="check"
                    initial={{ scale: 0.7, opacity: 0 }}
                    animate={{ scale: 1, opacity: 1 }}
                    exit={{ scale: 0.7, opacity: 0 }}
                    transition={{ duration: 0.15 }}
                    className="size-4 text-emerald-500"
                    viewBox="0 0 16 16"
                    fill="none"
                  >
                    <path d="M3 8l3.5 3.5L13 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
                  </motion.svg>
                ) : (
                  <motion.div key="copy" initial={{ scale: 0.7, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.7, opacity: 0 }} transition={{ duration: 0.15 }}>
                    <Copy className="size-4" aria-hidden />
                  </motion.div>
                )}
              </AnimatePresence>
            </button>
          )}
          {onClose && (
            <button
              type="button"
              onClick={onClose}
              aria-label="Close"
              className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            >
              <X className="size-4" aria-hidden />
            </button>
          )}
        </div>
      </div>

      {/* Error message */}
      <div className="px-4 py-3">
        <p
          className={cn(
            "text-xs leading-relaxed",
            isError ? "text-red-600 dark:text-red-400" : "text-amber-600 dark:text-amber-400"
          )}
        >
          {message}
        </p>
      </div>

      {/* Code frame */}
      {frame && (
        <div className="mx-4 mb-3 overflow-hidden rounded-xl border border-border bg-muted/40">
          <div className="flex items-center justify-between border-b border-border px-3 py-2">
            <span className="font-mono text-[11px] text-muted-foreground">{frame.file}</span>
            {onOpenFrame && (
              <button
                type="button"
                onClick={() => onOpenFrame(frame.file)}
                aria-label="Open in editor"
                className="rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground"
              >
                <ExternalLink className="size-3.5" aria-hidden />
              </button>
            )}
          </div>
          <div className="overflow-x-auto p-2 font-mono text-[11px] leading-5">
            {frame.lines.map((line) => (
              <div
                key={line.number}
                className={cn(
                  "flex gap-3 rounded px-1",
                  line.isError
                    ? "bg-red-50 dark:bg-red-950/40"
                    : ""
                )}
              >
                <span
                  className={cn(
                    "w-6 shrink-0 select-none text-right tabular-nums",
                    line.isError
                      ? "text-red-400 dark:text-red-500"
                      : "text-muted-foreground/50"
                  )}
                >
                  {line.isError ? ">" : ""}{line.number}
                </span>
                <span className="whitespace-pre text-muted-foreground">
                  <CodeContent content={line.content} isError={line.isError} />
                </span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Call stack */}
      {stack.length > 0 && (
        <div className="mx-4 mb-3 overflow-hidden rounded-xl border border-border">
          <button
            type="button"
            onClick={() => setStackOpen((v) => !v)}
            aria-expanded={stackOpen}
            className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/50"
          >
            <div className="flex items-center gap-2">
              <span className="text-[11px] font-semibold text-foreground">Call Stack</span>
              <span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground tabular-nums">
                {stack.length}
              </span>
            </div>
            <ChevronDown
              className={cn(
                "size-4 shrink-0 text-muted-foreground transition-transform",
                stackOpen && "rotate-180"
              )}
              aria-hidden
            />
          </button>

          <AnimatePresence initial={false}>
            {stackOpen && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: "auto", opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.18, ease: "easeInOut" }}
                className="overflow-hidden border-t border-border"
              >
                <div className="divide-y divide-border">
                  {stack.map((frame, i) => (
                    <div key={i} className="px-3 py-2">
                      <p className="text-[11px] font-medium text-foreground">{frame.name}</p>
                      {frame.context && (
                        <p className="text-[11px] text-muted-foreground">{frame.context}</p>
                      )}
                      {frame.file && (
                        <p className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">{frame.file}</p>
                      )}
                    </div>
                  ))}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      )}

      {/* Footer */}
      {onHelpful && (
        <div className="flex items-center justify-end gap-2 border-t border-border px-4 py-2.5">
          <span className="text-[11px] text-muted-foreground">Was this helpful?</span>
          <button
            type="button"
            onClick={() => handleHelpful(true)}
            aria-label="Yes, helpful"
            aria-pressed={helpfulVote === true}
            className={cn(
              "rounded-md p-1 transition-colors",
              helpfulVote === true
                ? "text-emerald-500"
                : "text-muted-foreground hover:text-foreground"
            )}
          >
            <ThumbsUp className="size-3.5" aria-hidden />
          </button>
          <button
            type="button"
            onClick={() => handleHelpful(false)}
            aria-label="Not helpful"
            aria-pressed={helpfulVote === false}
            className={cn(
              "rounded-md p-1 transition-colors",
              helpfulVote === false
                ? "text-red-500"
                : "text-muted-foreground hover:text-foreground"
            )}
          >
            <ThumbsDown className="size-3.5" aria-hidden />
          </button>
        </div>
      )}
    </div>
  );
}

function CodeContent({ content, isError }: { content: string; isError?: boolean }) {
  return (
    <span className={isError ? "text-foreground" : undefined}>
      {content}
    </span>
  );
}

A UX spec for this pattern — written for agents implementing or reusing it, not the code.

# Console Error Card

## Summary
A structured card that surfaces a browser console error or warning inside an AI product's interface. Shows a human-readable error message, an inline code frame pointing to the offending line, a collapsible call stack, and optional navigation across multiple errors — letting the user inspect, copy, and dismiss without leaving the conversation.

## When to use
- When an AI coding assistant detects a runtime error in the user's running application and needs to show it contextually alongside the conversation.
- When surfacing errors collected from a browser session, a test run, or a build log that the AI is helping the user debug.
- When there are multiple errors to navigate through (1/N pattern) and the user needs to triage them one at a time.

## When not to use
- Quota or rate-limit errors — use the Rate Limit pattern instead.
- Simple one-line error toasts that don't need code context — use a standard alert or toast.
- Full-screen error pages (unrecoverable crashes) — this card is for inline, dismissible inspection, not for blocking states.
- Compiler or build errors without a known line number — omit the code frame; don't show an empty frame.

## Anatomy
- **Top bar**: navigation arrows (previous / next) and a "1/N" counter for multi-error sessions; a severity badge ("Console Error" in red, "Console Warning" in amber); copy and close action buttons.
- **Error message**: the raw error text, rendered in the severity color. Should be scannable at a glance.
- **Code frame**: file path and line:col reference in the header; a small window of source lines with the error line highlighted and a ">" gutter indicator; an optional "Open in editor" affordance.
- **Call Stack**: a collapsible section with a frame count badge. Each frame shows the function name, an optional anonymous-context label, and the file path. Hidden by default to reduce visual weight.
- **Helpful footer**: thumbs-up / thumbs-down feedback, revealed only when `onHelpful` is wired up. Lets the product collect signal on error surface quality.

## Behavior
- Navigation arrows are disabled when at the first or last error; they are hidden entirely when `total` is 1.
- The call stack collapses with a height animation (Motion `AnimatePresence`). Opening it does not shift the surrounding layout unexpectedly — use overflow-hidden during transition.
- Copying triggers a brief checkmark-swap animation on the copy icon (≈1.5 s), then reverts. No toast is needed — the icon change is the confirmation.
- The helpful-vote buttons toggle `aria-pressed` and apply a color accent on selection; voting again on the same choice has no effect (idempotent).
- Closing fires `onClose` and is the caller's responsibility — the card itself does not unmount; wrap it in `AnimatePresence` if you need an exit animation.

## Code frame guidelines
- Show 3–6 lines of context around the error line; 2 lines before and 2 after is a good default.
- Always include the line number and column in the file header (`path/to/file.tsx (line:col) @ FunctionName`).
- Highlight only the single error line — highlighting a range suggests a range selection, which is misleading.
- If the source is not available, omit the `frame` prop entirely rather than showing a placeholder.

## Content guidelines
- Error message: verbatim from the console, no paraphrasing. The stack trace may rephrase, but the top-level message must be exact so the user can search for it.
- Badge label: "Console Error" or "Console Warning" — not "Runtime Error", "JS Error", or anything branded.
- Call stack frame names: use the function/component name as it appears in the source, not the mangled bundler name. If the frame is anonymous, show `<anonymous>` literally.
- "Was this helpful?" — only show this when the AI generated the error diagnosis. Don't ask for helpfulness on raw errors the AI simply forwarded.

## Accessibility
- The card root should have `role="alert"` when it appears dynamically so screen readers announce it immediately.
- Navigation buttons have `aria-label="Previous error"` / `aria-label="Next error"`.
- The call-stack toggle button has `aria-expanded` tracking the open state.
- Thumbs buttons have `aria-label` ("Yes, helpful" / "Not helpful") and `aria-pressed` for toggle semantics.
- The code frame is `aria-hidden` to screen readers if the full error message already conveys the problem — avoid reading out raw code lines.

## Related patterns
- Rate Limit — for quota-based errors, not runtime errors.
- Partial Response — for responses cut short by token or context limits.
- Terminal Stream — for streaming build/test output where errors surface in line.