For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

AI Profile 01

A profile dashboard with activity and usage charts.

Installation

Set up the @cobalt registry first, as described in Installation, then add the block:

$ pnpm dlx shadcn@latest add @cobalt/ai-profile-01

The example uses local sample data and renders the profile body without navigation or heading, inside a scrollable preview. Rows, heatmap cells, bars and the usage line animate in on mount, and every animation stops at its final readable state under prefers-reduced-motion.

Source

"use client";

import { RiCheckLine, RiEditBoxLine, RiShare2Line } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

import { AiProfileAgentsCard } from "@/components/blocks/ai-profile-01/components/ai-profile-agents-card";
import { profileCoverUrl } from "@/components/blocks/ai-profile-01/components/ai-profile-cover";
import { profileStats } from "@/components/blocks/ai-profile-01/components/ai-profile-data";
import { AiProfileHeatmap } from "@/components/blocks/ai-profile-01/components/ai-profile-heatmap";
import { AiProfileTokensCard } from "@/components/blocks/ai-profile-01/components/ai-profile-tokens-card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TooltipProvider } from "@/components/ui/tooltip";

// 500ms, 12px rise. Each row sets --delay in 40ms steps.
const rise = "ai-profile-01-rise";

function Row({
  step,
  className,
  children,
}: {
  step: number;
  className?: string;
  children: React.ReactNode;
}) {
  return (
    <div
      className={cn(rise, className)}
      style={{ "--delay": `${step * 40}ms` } as React.CSSProperties}
    >
      {children}
    </div>
  );
}

/**
 * A single-column profile: cover, identity, contribution total, stat tiles and
 * an activity heatmap, then an agents card and a token usage card. It carries
 * no navigation or heading, so it drops into an app shell or any page. Pass
 * `popupClassName` (a theme scope) so tooltips pick up the same tokens.
 */
export function AiProfile({
  className,
  popupClassName,
}: {
  className?: string;
  popupClassName?: string;
}) {
  const [name, setName] = React.useState("Alex Morgan");
  const [editing, setEditing] = React.useState(false);
  const [copied, setCopied] = React.useState(false);
  React.useEffect(() => {
    if (!copied) {
      return;
    }
    const timer = window.setTimeout(() => setCopied(false), 1800);
    return () => window.clearTimeout(timer);
  }, [copied]);
  const share = async () => {
    try {
      await navigator.clipboard.writeText(window.location.href);
    } catch {
      // Clipboard can be blocked; the button still confirms the action.
    }
    setCopied(true);
  };
  return (
    <TooltipProvider delay={300}>
      <div
        className={cn(
          "@container mx-auto flex w-full max-w-[680px] min-w-0 flex-col gap-4",
          className
        )}
      >
        <Row step={0}>
          <Card radius={24} size="flush" variant="outline">
            {/* oxlint-disable-next-line next/no-img-element -- blocks stay framework-agnostic, so no next/image */}
            <img
              alt="Illustrated cover: snowy peaks under a low sun"
              className="h-28 w-full object-cover @xl:h-[165px]"
              src={profileCoverUrl}
            />
            <div className="flex flex-col gap-4 p-4 pt-0">
              <div className="flex items-start justify-between gap-2">
                <Avatar className="-mt-10 size-20">
                  <AvatarFallback>
                    {(name.trim() || "?").charAt(0).toUpperCase()}
                  </AvatarFallback>
                </Avatar>
                <div className="mt-5 -mb-3 mr-1 flex gap-2">
                  <Button variant="outline" size="sm" onClick={share}>
                    {copied ? (
                      <RiCheckLine data-icon="inline-start" />
                    ) : (
                      <RiShare2Line data-icon="inline-start" />
                    )}
                    {copied ? "Copied" : "Share"}
                  </Button>
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => setEditing((value) => !value)}
                  >
                    {editing ? (
                      <RiCheckLine data-icon="inline-start" />
                    ) : (
                      <RiEditBoxLine data-icon="inline-start" />
                    )}
                    {editing ? "Done" : "Edit"}
                  </Button>
                </div>
              </div>
              <div className="-mt-1 flex flex-col gap-0.5">
                {editing ? (
                  <div className="flex max-w-60 flex-col gap-1">
                    <Label htmlFor="ai-profile-display-name">
                      Display name
                    </Label>
                    <Input
                      id="ai-profile-display-name"
                      size="sm"
                      value={name}
                      onChange={(event) => setName(event.target.value)}
                    />
                  </div>
                ) : (
                  <h2 className="text-xl font-medium">
                    {name.trim() || "Your name"}
                  </h2>
                )}
                <p className="flex items-center gap-1.5 text-base text-muted-foreground">
                  @alexm
                  <Badge variant="caption" tone="neutral">
                    Pro
                  </Badge>
                </p>
              </div>
              <div className="-mb-1 flex flex-col gap-1">
                <p className="text-sm text-muted-foreground">
                  Contributions this year
                </p>
                <div className="flex items-center gap-2">
                  <span className="text-2xl leading-7 font-medium tabular-nums">
                    $5,318
                  </span>
                  <Badge variant="label" tone="purple">
                    +9.4%
                  </Badge>
                </div>
              </div>
              <dl className="grid grid-cols-2 gap-2 @xl:grid-cols-4">
                {profileStats.map((stat, index) => (
                  <Row key={stat.caption} step={index + 1} className="grid">
                    <Card variant="muted" size="xs" radius={12}>
                      <CardContent className="flex flex-col gap-0">
                        <dd className="text-sm leading-4.5 font-medium">
                          {stat.value}
                        </dd>
                        <dt className="text-xs leading-4.5 text-muted-foreground">
                          {stat.caption}
                        </dt>
                      </CardContent>
                    </Card>
                  </Row>
                ))}
              </dl>
              <AiProfileHeatmap popupClassName={popupClassName} />
            </div>
          </Card>
        </Row>
        <Row step={1}>
          <AiProfileAgentsCard />
        </Row>
        <Row step={2}>
          <AiProfileTokensCard />
        </Row>
      </div>
    </TooltipProvider>
  );
}