-

Indexing Progress

PreviousNext

A progress panel for document ingestion, chunking, embedding, and knowledge base rebuilds.

Installation

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

Component

components/ui/indexing-progress.tsx
"use client"

import * as React from "react"
import {
  AlertCircleIcon,
  CheckCircle2Icon,
  FileTextIcon,
  Loader2Icon,
  PauseCircleIcon,
  RotateCcwIcon,
} 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 { Progress } from "@/components/ui/progress"

export type IndexingProgressStatus =
  | "idle"
  | "indexing"
  | "complete"
  | "failed"
  | "paused"

export interface IndexingProgressProps
  extends Omit<React.ComponentProps<typeof Card>, "title"> {
  title?: React.ReactNode
  stage?: React.ReactNode
  progress: number
  status?: IndexingProgressStatus
  currentFile?: React.ReactNode
  processedDocuments?: number
  totalDocuments?: number
  totalChunks?: number
  failedDocuments?: number
  onRetry?: () => void
}

const statusConfig = {
  idle: {
    label: "Idle",
    icon: FileTextIcon,
    className: "bg-muted text-muted-foreground",
  },
  indexing: {
    label: "Indexing",
    icon: Loader2Icon,
    className: "bg-blue-500/10 text-blue-700 dark:text-blue-300",
  },
  complete: {
    label: "Complete",
    icon: CheckCircle2Icon,
    className: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
  },
  failed: {
    label: "Failed",
    icon: AlertCircleIcon,
    className: "bg-destructive/10 text-destructive",
  },
  paused: {
    label: "Paused",
    icon: PauseCircleIcon,
    className: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
  },
} satisfies Record<
  IndexingProgressStatus,
  {
    label: string
    icon: React.ComponentType<{ className?: string }>
    className: string
  }
>

function clampProgress(value: number) {
  return Math.max(0, Math.min(100, Math.round(value)))
}

export function IndexingProgress({
  title = "Knowledge indexing",
  stage,
  progress,
  status = "indexing",
  currentFile,
  processedDocuments,
  totalDocuments,
  totalChunks,
  failedDocuments,
  onRetry,
  className,
  ...props
}: IndexingProgressProps) {
  const normalizedProgress = clampProgress(progress)
  const config = statusConfig[status]
  const StatusIcon = config.icon
  const documentLabel =
    typeof processedDocuments === "number" && typeof totalDocuments === "number"
      ? `${processedDocuments}/${totalDocuments} documents`
      : null

  return (
    <Card className={cn("w-full rounded-lg py-0", className)} {...props}>
      <CardContent className="space-y-4 p-4">
        <div className="flex items-start justify-between gap-3">
          <div className="min-w-0">
            <h3 className="truncate text-sm font-medium">{title}</h3>
            {stage ? (
              <p className="text-muted-foreground mt-1 text-xs leading-5">
                {stage}
              </p>
            ) : null}
          </div>
          <Badge
            variant="secondary"
            className={cn("h-6 rounded-md border-0", config.className)}
          >
            <StatusIcon
              className={cn(
                "size-3.5",
                status === "indexing" && "animate-spin"
              )}
            />
            {config.label}
          </Badge>
        </div>

        <div className="space-y-2">
          <div className="flex items-center justify-between gap-3 text-xs">
            <span className="text-muted-foreground">Progress</span>
            <span className="font-medium tabular-nums">
              {normalizedProgress}%
            </span>
          </div>
          <Progress value={normalizedProgress} className="h-2" />
        </div>

        {currentFile ? (
          <div className="bg-muted/60 flex min-h-9 items-center gap-2 rounded-md px-3 text-xs">
            <FileTextIcon className="text-muted-foreground size-3.5 shrink-0" />
            <span className="text-muted-foreground shrink-0">Current</span>
            <span className="truncate font-medium">{currentFile}</span>
          </div>
        ) : null}

        <div className="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
          {documentLabel ? (
            <div className="rounded-md border p-2">
              <div className="text-muted-foreground">Documents</div>
              <div className="mt-1 font-medium tabular-nums">
                {documentLabel}
              </div>
            </div>
          ) : null}
          {typeof totalChunks === "number" ? (
            <div className="rounded-md border p-2">
              <div className="text-muted-foreground">Chunks</div>
              <div className="mt-1 font-medium tabular-nums">
                {totalChunks.toLocaleString()}
              </div>
            </div>
          ) : null}
          {typeof failedDocuments === "number" ? (
            <div className="rounded-md border p-2">
              <div className="text-muted-foreground">Failed</div>
              <div className="mt-1 font-medium tabular-nums">
                {failedDocuments}
              </div>
            </div>
          ) : null}
          {onRetry ? (
            <Button
              type="button"
              variant="outline"
              size="sm"
              className="h-auto min-h-12 justify-start gap-2 rounded-md"
              onClick={onRetry}
            >
              <RotateCcwIcon className="size-3.5" />
              Retry
            </Button>
          ) : null}
        </div>
      </CardContent>
    </Card>
  )
}

Usage

import { IndexingProgress } from "@/components/ui/indexing-progress"
 
export function Example() {
  return (
    <IndexingProgress
      progress={68}
      status="indexing"
      currentFile="support-playbook.pdf"
      processedDocuments={17}
      totalDocuments={25}
      totalChunks={1842}
    />
  )
}

Props

PropTypeDefault
progressnumberrequired
status"idle" | "indexing" | "complete" | "failed" | "paused""indexing"
titleReactNode"Knowledge indexing"
stageReactNode-
currentFileReactNode-
processedDocumentsnumber-
totalDocumentsnumber-
totalChunksnumber-
failedDocumentsnumber-
onRetry() => void-