-

Dataset Card

PreviousNext

A dataset summary card for knowledge bases, folders, document counts, and indexing state.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/dataset-card.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/dataset-card.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/dataset-card.json

Component

components/ui/dataset-card.tsx
"use client"

import * as React from "react"
import {
  DatabaseIcon,
  FileTextIcon,
  FolderIcon,
  LockIcon,
  MoreHorizontalIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export type DatasetCardStatus = "ready" | "indexing" | "failed" | "empty"

export interface DatasetCardAction {
  label: React.ReactNode
  icon?: React.ReactNode
  onSelect?: () => void
}

export interface DatasetCardProps
  extends Omit<React.ComponentProps<typeof Card>, "title"> {
  title: React.ReactNode
  description?: React.ReactNode
  status?: DatasetCardStatus
  documentCount?: number
  segmentCount?: number
  updatedAt?: React.ReactNode
  visibility?: "private" | "workspace" | "public"
  tags?: React.ReactNode[]
  actions?: DatasetCardAction[]
  href?: string
  onOpen?: () => void
}

const statusClassName: Record<DatasetCardStatus, string> = {
  ready: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
  indexing: "bg-blue-500/10 text-blue-700 dark:text-blue-300",
  failed: "bg-destructive/10 text-destructive",
  empty: "bg-muted text-muted-foreground",
}

const statusLabel: Record<DatasetCardStatus, string> = {
  ready: "Ready",
  indexing: "Indexing",
  failed: "Failed",
  empty: "Empty",
}

function Metric({
  icon,
  label,
}: {
  icon: React.ReactNode
  label: React.ReactNode
}) {
  return (
    <span className="text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs">
      <span className="[&_svg]:size-3.5">{icon}</span>
      <span className="truncate">{label}</span>
    </span>
  )
}

export function DatasetCard({
  title,
  description,
  status = "ready",
  documentCount,
  segmentCount,
  updatedAt,
  visibility = "workspace",
  tags = [],
  actions = [],
  href,
  onOpen,
  className,
  ...props
}: DatasetCardProps) {
  const content = (
    <CardContent className="flex min-h-48 flex-col gap-4 p-4">
      <div className="flex items-start gap-3">
        <div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-lg">
          <DatabaseIcon className="size-5" />
        </div>
        <div className="min-w-0 flex-1">
          <div className="flex min-w-0 flex-wrap items-center gap-2">
            <h3 className="truncate text-sm font-medium">{title}</h3>
            <Badge
              variant="secondary"
              className={cn("h-6 rounded-md border-0", statusClassName[status])}
            >
              {statusLabel[status]}
            </Badge>
          </div>
          {description ? (
            <p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
              {description}
            </p>
          ) : null}
        </div>
        {actions.length ? (
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button
                type="button"
                variant="ghost"
                size="icon"
                className="size-8 shrink-0 rounded-md"
                onClick={(event) => event.preventDefault()}
                aria-label="Open dataset actions"
              >
                <MoreHorizontalIcon className="size-4" />
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              {actions.map((action, index) => (
                <DropdownMenuItem
                  key={index}
                  onSelect={(event) => {
                    event.preventDefault()
                    action.onSelect?.()
                  }}
                >
                  {action.icon ? (
                    <span className="[&_svg]:size-4">{action.icon}</span>
                  ) : null}
                  {action.label}
                </DropdownMenuItem>
              ))}
            </DropdownMenuContent>
          </DropdownMenu>
        ) : null}
      </div>

      <div className="grid grid-cols-2 gap-2">
        {typeof documentCount === "number" ? (
          <div className="rounded-md border p-2">
            <div className="text-muted-foreground text-xs">Documents</div>
            <div className="mt-1 text-sm font-medium tabular-nums">
              {documentCount.toLocaleString()}
            </div>
          </div>
        ) : null}
        {typeof segmentCount === "number" ? (
          <div className="rounded-md border p-2">
            <div className="text-muted-foreground text-xs">Segments</div>
            <div className="mt-1 text-sm font-medium tabular-nums">
              {segmentCount.toLocaleString()}
            </div>
          </div>
        ) : null}
      </div>

      {tags.length ? (
        <div className="flex flex-wrap gap-1.5">
          {tags.map((tag, index) => (
            <Badge
              key={index}
              variant="secondary"
              className="h-6 rounded-md px-2 font-normal"
            >
              {tag}
            </Badge>
          ))}
        </div>
      ) : null}

      <div className="mt-auto flex flex-wrap items-center justify-between gap-2 border-t pt-3">
        <Metric
          icon={visibility === "private" ? <LockIcon /> : <FolderIcon />}
          label={visibility}
        />
        {updatedAt ? (
          <Metric icon={<FileTextIcon />} label={updatedAt} />
        ) : null}
      </div>
    </CardContent>
  )

  return (
    <Card
      className={cn(
        "hover:border-primary/40 hover:bg-accent/20 overflow-hidden rounded-lg py-0 transition-colors",
        (href || onOpen) && "cursor-pointer",
        className
      )}
      onClick={onOpen}
      {...props}
    >
      {href ? (
        <a href={href} className="block h-full">
          {content}
        </a>
      ) : (
        content
      )}
    </Card>
  )
}

Usage

import { DatasetCard } from "@/components/ui/dataset-card"
 
export function Example() {
  return (
    <DatasetCard
      title="Support Knowledge"
      status="ready"
      documentCount={128}
      segmentCount={8420}
      tags={["Support", "Production"]}
    />
  )
}

Props

PropTypeDefault
titleReactNoderequired
descriptionReactNode-
status"ready" | "indexing" | "failed" | "empty""ready"
documentCountnumber-
segmentCountnumber-
updatedAtReactNode-
visibility"private" | "workspace" | "public""workspace"
tagsReactNode[][]
actionsDatasetCardAction[][]
hrefstring-
onOpen() => void-