Getting Started
Components
- Agent Status
- Agent Template Card
- API Key Card
- Audio Player
- Bar Visualizer
- Channel Status Card
- Chat Composer
- Code Editor
- Condition Builder
- Confirm Dialog
- Conversation
- Conversation Bar
- Dataset Card
- Document Status Badge
- Extraction Strategy Select
- File Icon
- File Upload
- Indexing Progress
- Live Waveform
- Matrix
- Message
- Mic Selector
- Model Diff View
- Model Selector
- Model Type Filter
- Node Catalog
- Option Editor
- Orb
- Output Variables View
- Package Card
- Prompt Editor
- Provider Status Card
- Radio Card
- Resource Card
- Resource Sidebar
- Response
- Run Node List
- Scrub Bar
- Segment Card
- Shimmering Text
- Speech Input
- Token Trend Chart
- Tool Call Card
- Transcript Viewer
- Usage Stat Card
- Variable Binding Editor
- Variable Selector
- Voice Button
- Voice Picker
- Waveform
- Workflow Node Card
- Workspace Selector
Upload ZGI resources
Drop workflow briefs, datasets, or knowledge files before running an agent.
workflow-brief.md18.0 KB
large-dataset.csv32.4 MB
File is larger than 25 MB
"use client"
import { useState } from "react"
import {
FileUpload,
type FileUploadItem,
} from "@/components/ui/file-upload"
function demoFile(name: string, size: number, type: string) {
const file = new File(["zgi"], name, {
type,
lastModified: Date.now(),
})
Object.defineProperty(file, "size", {
value: size,
})
return file
}
const initialItems: FileUploadItem[] = [
{
id: "workflow-brief",
file: demoFile("workflow-brief.md", 18400, "text/markdown"),
progress: 100,
status: "success",
},
{
id: "large-dataset",
file: demoFile("large-dataset.csv", 34000000, "text/csv"),
progress: 100,
status: "error",
error: "File is larger than 25 MB",
},
]
export function FileUploadDemo() {
const [items, setItems] = useState<FileUploadItem[]>(initialItems)
return (
<FileUpload
value={items}
onValueChange={setItems}
accept=".csv,.md,.pdf,.txt"
maxFiles={5}
maxSizeMB={25}
title="Upload ZGI resources"
description="Drop workflow briefs, datasets, or knowledge files before running an agent."
className="w-full max-w-2xl"
/>
)
}
Installation
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/file-upload.jsonpnpm dlx shadcn@latest add https://ui.zgi.ai/r/file-upload.json
yarn dlx shadcn@latest add https://ui.zgi.ai/r/file-upload.jsonComponent
"use client"
import * as React from "react"
import {
CheckCircle2Icon,
Loader2Icon,
Trash2Icon,
UploadCloudIcon,
XCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { FileIcon } from "@/components/ui/file-icon"
import { Progress } from "@/components/ui/progress"
export type FileUploadItemStatus = "pending" | "uploading" | "success" | "error"
export interface FileUploadItem {
id: string
file: File
progress?: number
status?: FileUploadItemStatus
error?: string
}
export interface FileUploadProps
extends Omit<React.ComponentProps<"div">, "onChange" | "title"> {
value?: FileUploadItem[]
onValueChange?: (items: FileUploadItem[]) => void
onFilesAdded?: (files: File[]) => void
accept?: string
maxFiles?: number
maxSizeMB?: number
disabled?: boolean
multiple?: boolean
title?: React.ReactNode
description?: React.ReactNode
browseText?: string
emptyText?: string
}
function formatBytes(bytes: number) {
if (bytes === 0) return "0 B"
const units = ["B", "KB", "MB", "GB"]
const index = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1
)
return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`
}
function buildUploadItem(file: File): FileUploadItem {
return {
id: `${file.name}-${file.size}-${file.lastModified}`,
file,
progress: 0,
status: "pending",
}
}
export function FileUpload({
value,
onValueChange,
onFilesAdded,
accept,
maxFiles = 5,
maxSizeMB = 25,
disabled = false,
multiple = true,
title = "Drop files here",
description = "Attach files for a ZGI agent, workflow, or dataset.",
browseText = "Browse files",
emptyText = "No files selected",
className,
...props
}: FileUploadProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const [internalItems, setInternalItems] = React.useState<FileUploadItem[]>([])
const [isDragging, setIsDragging] = React.useState(false)
const items = value ?? internalItems
const setItems = onValueChange ?? setInternalItems
const maxBytes = maxSizeMB * 1024 * 1024
function addFiles(fileList: FileList | File[]) {
if (disabled) return
const incoming = Array.from(fileList)
const existingIds = new Set(items.map((item) => item.id))
const nextItems = [...items]
for (const file of incoming) {
if (nextItems.length >= maxFiles) break
const item = buildUploadItem(file)
if (existingIds.has(item.id)) continue
if (file.size > maxBytes) {
nextItems.push({
...item,
progress: 100,
status: "error",
error: `File is larger than ${maxSizeMB} MB`,
})
} else {
nextItems.push(item)
}
}
setItems(nextItems)
onFilesAdded?.(incoming)
}
function removeItem(id: string) {
setItems(items.filter((item) => item.id !== id))
}
return (
<div className={cn("space-y-3", className)} {...props}>
<div
role="button"
tabIndex={disabled ? -1 : 0}
aria-disabled={disabled}
onClick={() => inputRef.current?.click()}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
inputRef.current?.click()
}
}}
onDragOver={(event) => {
event.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={(event) => {
event.preventDefault()
setIsDragging(false)
addFiles(event.dataTransfer.files)
}}
className={cn(
"border-border bg-card text-card-foreground flex min-h-36 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed p-5 text-center transition-colors",
"hover:border-primary/60 hover:bg-accent/30",
"focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none",
isDragging && "border-primary bg-primary/5",
disabled && "pointer-events-none opacity-50"
)}
>
<input
ref={inputRef}
type="file"
accept={accept}
multiple={multiple}
disabled={disabled}
className="sr-only"
onChange={(event) => {
if (event.target.files) addFiles(event.target.files)
event.target.value = ""
}}
/>
<UploadCloudIcon className="text-primary mb-3 size-7" />
<div className="text-sm leading-5 font-medium">{title}</div>
<div className="text-muted-foreground mt-1 max-w-sm text-sm leading-6">
{description}
</div>
<Button
type="button"
variant="outline"
size="sm"
className="mt-4 rounded-md"
disabled={disabled}
onClick={(event) => {
event.stopPropagation()
inputRef.current?.click()
}}
>
{browseText}
</Button>
</div>
<div className="space-y-2">
{items.length === 0 ? (
<div className="text-muted-foreground border-border rounded-lg border px-3 py-2 text-sm">
{emptyText}
</div>
) : (
items.map((item) => {
const status = item.status ?? "pending"
const progress = item.progress ?? (status === "success" ? 100 : 0)
return (
<div
key={item.id}
className="border-border bg-card flex items-center gap-3 rounded-lg border p-3"
>
<FileIcon filename={item.file.name} className="shrink-0" />
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{item.file.name}
</span>
<span className="text-muted-foreground shrink-0 text-xs">
{formatBytes(item.file.size)}
</span>
</div>
{status === "uploading" ? (
<Progress value={progress} className="mt-2 h-1.5" />
) : item.error ? (
<p className="text-destructive mt-1 text-xs">
{item.error}
</p>
) : null}
</div>
{status === "uploading" ? (
<Loader2Icon className="text-muted-foreground size-4 animate-spin" />
) : status === "success" ? (
<CheckCircle2Icon className="text-primary size-4" />
) : status === "error" ? (
<XCircleIcon className="text-destructive size-4" />
) : null}
<Button
type="button"
variant="ghost"
size="icon"
className="size-8 rounded-md"
onClick={() => removeItem(item.id)}
aria-label={`Remove ${item.file.name}`}
>
<Trash2Icon className="size-4" />
</Button>
</div>
)
})
)}
</div>
</div>
)
}
Usage
import { FileUpload } from "@/components/ui/file-upload"
export function Example() {
return (
<FileUpload
accept=".csv,.md,.pdf"
maxFiles={5}
maxSizeMB={25}
onFilesAdded={(files) => console.log(files)}
/>
)
}Props
| Prop | Type | Default |
|---|---|---|
| value | FileUploadItem[] | - |
| onValueChange | (items: FileUploadItem[]) => void | - |
| onFilesAdded | (files: File[]) => void | - |
| accept | string | - |
| maxFiles | number | 5 |
| maxSizeMB | number | 25 |
| multiple | boolean | true |
| disabled | boolean | false |
Deploy and Scale Agents with ZGI
ZGI delivers the infrastructure and developer experience you need to ship reliable audio & agent applications at scale.
Talk to an expert