{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "agent-composer", "title": "Agent Composer", "description": "Controlled text and multimodal file composer with bounded validation, previews, send and stop controls, and injectable tool slots", "dependencies": ["daisyui", "lucide-react"], "files": [ { "path": "registry/default/ui/agent-composer/agent-composer-types.ts", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\n\nexport const AGENT_COMPOSER_DEFAULT_MAX_FILES = 5;\nexport const AGENT_COMPOSER_DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;\nexport const AGENT_COMPOSER_DEFAULT_MAX_TOTAL_SIZE = 20 * 1024 * 1024;\n\nexport type AgentComposerStatus = 'ready' | 'submitting' | 'streaming' | 'error';\n\nexport type AgentComposerMessages = {\n\tattachAudio: string;\n\tattachFile: string;\n\tattachImage: string;\n\tattachmentsLabel: string;\n\taudioPreview: (name: string) => string;\n\tcomposerLabel: string;\n\tdropFiles: string;\n\tfileCountError: (maxFiles: number) => string;\n\tfileSizeError: (name: string, maxSize: string) => string;\n\timagePreview: (name: string) => string;\n\tplaceholder: string;\n\tremoveAttachment: (name: string) => string;\n\tselectAudio: string;\n\tselectFile: string;\n\tselectImage: string;\n\tsend: string;\n\tstop: string;\n\ttotalSizeError: (maxSize: string) => string;\n\tunnamedFile: string;\n};\n\nexport const defaultAgentComposerMessages: AgentComposerMessages = {\n\tattachAudio: '添加音频',\n\tattachFile: '添加文件',\n\tattachImage: '添加图片',\n\tattachmentsLabel: '附件',\n\taudioPreview: (name) => `音频预览:${name}`,\n\tcomposerLabel: '消息编辑器',\n\tdropFiles: '松开以添加附件',\n\tfileCountError: (maxFiles) => `最多添加 ${maxFiles} 个文件。`,\n\tfileSizeError: (name, maxSize) => `${name} 超过单个文件 ${maxSize} 的限制。`,\n\timagePreview: (name) => `图片预览:${name}`,\n\tplaceholder: '输入消息…',\n\tremoveAttachment: (name) => `移除附件:${name}`,\n\tselectAudio: '选择音频文件',\n\tselectFile: '选择文件',\n\tselectImage: '选择图片文件',\n\tsend: '发送',\n\tstop: '停止生成',\n\ttotalSizeError: (maxSize) => `附件总大小不能超过 ${maxSize}。`,\n\tunnamedFile: '未命名文件',\n};\n\nexport function resolveAgentComposerMessages(messages?: Partial): AgentComposerMessages {\n\treturn { ...defaultAgentComposerMessages, ...messages };\n}\n\nexport type AgentComposerLimits = {\n\tmaxFileSize?: number;\n\tmaxFiles?: number;\n\tmaxTotalSize?: number;\n};\n\nexport type AgentComposerResolvedLimits = {\n\tmaxFileSize: number;\n\tmaxFiles: number;\n\tmaxTotalSize: number;\n};\n\nexport type AgentComposerErrorCode = 'file-count' | 'file-size' | 'total-size';\n\nexport type AgentComposerError = {\n\tcode: AgentComposerErrorCode;\n\tfiles: File[];\n\tmessage: string;\n};\n\nexport type AgentComposerSubmitValue = {\n\tfiles: File[];\n\ttext: string;\n};\n\nexport type AgentComposerProps = Omit, 'onError' | 'onSubmit'> &\n\tAgentComposerLimits & {\n\t\taccept?: string;\n\t\tdisabled?: boolean;\n\t\terror?: ReactNode;\n\t\tfiles: readonly File[];\n\t\tleading?: ReactNode;\n\t\tmessages?: Partial;\n\t\tmicrophone?: ReactNode;\n\t\tmodel?: ReactNode;\n\t\tonError?: (error: AgentComposerError) => void;\n\t\tonFilesChange: (files: File[]) => void;\n\t\tonStop?: () => void;\n\t\tonSubmit: (value: AgentComposerSubmitValue) => void;\n\t\tonValueChange: (value: string) => void;\n\t\tsettings?: ReactNode;\n\t\tstatus?: AgentComposerStatus;\n\t\ttextareaProps?: Omit, 'disabled' | 'value'>;\n\t\ttrailing?: ReactNode;\n\t\tvalue: string;\n\t};\n", "type": "registry:lib", "target": "@components/agent-composer/agent-composer-types.ts" }, { "path": "registry/default/ui/agent-composer/agent-composer-files.ts", "content": "import {\n\tAGENT_COMPOSER_DEFAULT_MAX_FILE_SIZE,\n\tAGENT_COMPOSER_DEFAULT_MAX_FILES,\n\tAGENT_COMPOSER_DEFAULT_MAX_TOTAL_SIZE,\n\ttype AgentComposerError,\n\ttype AgentComposerLimits,\n\ttype AgentComposerMessages,\n\ttype AgentComposerResolvedLimits,\n} from './agent-composer-types';\n\nexport type AgentComposerFileValidation = {\n\taccepted: File[];\n\terrors: AgentComposerError[];\n};\n\nexport function resolveAgentComposerLimits(limits: AgentComposerLimits): AgentComposerResolvedLimits {\n\treturn {\n\t\tmaxFiles: boundedInteger(limits.maxFiles, AGENT_COMPOSER_DEFAULT_MAX_FILES),\n\t\tmaxFileSize: boundedInteger(limits.maxFileSize, AGENT_COMPOSER_DEFAULT_MAX_FILE_SIZE),\n\t\tmaxTotalSize: boundedInteger(limits.maxTotalSize, AGENT_COMPOSER_DEFAULT_MAX_TOTAL_SIZE),\n\t};\n}\n\nexport function validateAgentComposerControlledFiles(\n\tfiles: readonly File[],\n\tlimits: AgentComposerResolvedLimits,\n\tmessages: AgentComposerMessages,\n): AgentComposerFileValidation {\n\treturn validateAgentComposerFiles([], files, limits, messages);\n}\n\nexport function validateAgentComposerFiles(\n\tcurrent: readonly File[],\n\tincoming: Iterable,\n\tlimits: AgentComposerResolvedLimits,\n\tmessages: AgentComposerMessages,\n): AgentComposerFileValidation {\n\tconst accepted: File[] = [];\n\tconst errors: AgentComposerError[] = [];\n\tconst oversized: File[] = [];\n\tconst overCount: File[] = [];\n\tconst overTotal: File[] = [];\n\tlet totalSize = current.reduce((total, file) => total + finiteSize(file.size), 0);\n\tlet count = current.length;\n\n\tfor (const file of incoming) {\n\t\tconst size = safeFileSize(file);\n\t\tif (size === undefined || size > limits.maxFileSize) {\n\t\t\toversized.push(file);\n\t\t\tcontinue;\n\t\t}\n\t\tif (count >= limits.maxFiles) {\n\t\t\toverCount.push(file);\n\t\t\tcontinue;\n\t\t}\n\t\tif (totalSize + size > limits.maxTotalSize) {\n\t\t\toverTotal.push(file);\n\t\t\tcontinue;\n\t\t}\n\t\taccepted.push(file);\n\t\tcount += 1;\n\t\ttotalSize += size;\n\t}\n\n\tfor (const file of oversized) {\n\t\terrors.push({\n\t\t\tcode: 'file-size',\n\t\t\tfiles: [file],\n\t\t\tmessage: messages.fileSizeError(file.name || messages.unnamedFile, formatAgentComposerBytes(limits.maxFileSize)),\n\t\t});\n\t}\n\tif (overCount.length > 0) {\n\t\terrors.push({ code: 'file-count', files: overCount, message: messages.fileCountError(limits.maxFiles) });\n\t}\n\tif (overTotal.length > 0) {\n\t\terrors.push({\n\t\t\tcode: 'total-size',\n\t\t\tfiles: overTotal,\n\t\t\tmessage: messages.totalSizeError(formatAgentComposerBytes(limits.maxTotalSize)),\n\t\t});\n\t}\n\treturn { accepted, errors };\n}\n\nexport function formatAgentComposerBytes(bytes: number): string {\n\tconst size = finiteSize(bytes);\n\tif (size >= 1024 * 1024) return `${trimNumber(size / (1024 * 1024))} MiB`;\n\tif (size >= 1024) return `${trimNumber(size / 1024)} KiB`;\n\treturn `${size} B`;\n}\n\nfunction safeFileSize(file: File): number | undefined {\n\ttry {\n\t\treturn Number.isFinite(file.size) && file.size >= 0 ? Math.floor(file.size) : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction finiteSize(value: number): number {\n\treturn Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;\n}\n\nfunction boundedInteger(value: number | undefined, fallback: number): number {\n\treturn value === undefined || !Number.isFinite(value) ? fallback : Math.max(0, Math.floor(value));\n}\n\nfunction trimNumber(value: number): string {\n\treturn Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\\.0$/, '');\n}\n", "type": "registry:lib", "target": "@components/agent-composer/agent-composer-files.ts" }, { "path": "registry/default/ui/agent-composer/agent-composer-attachment.tsx", "content": "'use client';\n\nimport { FileText, Music2, X } from 'lucide-react';\nimport type { ComponentPropsWithRef } from 'react';\nimport { useEffect, useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { formatAgentComposerBytes } from './agent-composer-files';\nimport type { AgentComposerMessages } from './agent-composer-types';\n\nexport type AgentComposerAttachmentProps = Omit, 'children'> & {\n\tfile: File;\n\tmessages: AgentComposerMessages;\n\tonRemove: () => void;\n};\n\nexport function AgentComposerAttachment({\n\tclassName,\n\tfile,\n\tmessages,\n\tonRemove,\n\t...props\n}: AgentComposerAttachmentProps) {\n\tconst mediaKind = resolveMediaKind(file.type);\n\tconst objectUrl = useAgentComposerObjectUrl(file, mediaKind !== 'file');\n\tconst name = file.name || messages.unnamedFile;\n\treturn (\n\t\t\n\t\t\t{mediaKind === 'image' && objectUrl ? (\n\t\t\t\t\n\t\t\t) : null}\n\t\t\t{mediaKind === 'audio' ?