diff --git a/.changeset/prompt-input-accept-extensions.md b/.changeset/prompt-input-accept-extensions.md new file mode 100644 index 00000000..76ac5b6b --- /dev/null +++ b/.changeset/prompt-input-accept-extensions.md @@ -0,0 +1,5 @@ +--- +"ai-elements": patch +--- + +Match file extension patterns such as `.pdf` in the PromptInput `accept` prop diff --git a/apps/docs/content/components/(chatbot)/prompt-input.mdx b/apps/docs/content/components/(chatbot)/prompt-input.mdx index ad2c9f5b..c8487f80 100644 --- a/apps/docs/content/components/(chatbot)/prompt-input.mdx +++ b/apps/docs/content/components/(chatbot)/prompt-input.mdx @@ -283,7 +283,7 @@ Buttons can display tooltips with optional keyboard shortcut hints. Hover over t }, accept: { description: - 'File types to accept (e.g., "image/*"). Leave undefined for any.', + 'File types to accept, as MIME types or extensions (e.g., "image/*" or ".pdf"). Leave undefined for any.', type: "string", }, multiple: { diff --git a/packages/elements/__tests__/prompt-input.test.tsx b/packages/elements/__tests__/prompt-input.test.tsx index d961a3ba..6af997a4 100644 --- a/packages/elements/__tests__/prompt-input.test.tsx +++ b/packages/elements/__tests__/prompt-input.test.tsx @@ -1685,6 +1685,73 @@ describe("file validation", () => { }); }); + it("matches file extension patterns against the file name", async () => { + setupPromptInputTests(); + const onSubmit = vi.fn(); + const onError = vi.fn(); + const user = userEvent.setup(); + + // Browsers often report an empty or generic type for these files + const sheetFile = new File(["sheet"], "report.xlsx", { type: "" }); + const csvFile = new File(["a,b"], "DATA.CSV", { + type: "application/octet-stream", + }); + const textFile = new File(["text"], "notes.txt", { type: "text/plain" }); + + const AttachmentConsumer = () => { + const attachments = usePromptInputAttachments(); + return ( + <> + + + +
{attachments.files.length}
+ + ); + }; + + render( + + + + + + + ); + + await user.click(screen.getByTestId("add-sheet")); + expect(screen.getByTestId("count")).toHaveTextContent("1"); + + await user.click(screen.getByTestId("add-csv")); + expect(screen.getByTestId("count")).toHaveTextContent("2"); + expect(onError).not.toHaveBeenCalled(); + + await user.click(screen.getByTestId("add-text")); + expect(screen.getByTestId("count")).toHaveTextContent("2"); + expect(onError).toHaveBeenCalledWith({ + code: "accept", + message: expect.any(String), + }); + }); + it("handles accept with extra whitespace in patterns", async () => { setupPromptInputTests(); const onSubmit = vi.fn(); diff --git a/packages/elements/src/prompt-input.tsx b/packages/elements/src/prompt-input.tsx index 412c846d..8bdf9101 100644 --- a/packages/elements/src/prompt-input.tsx +++ b/packages/elements/src/prompt-input.tsx @@ -490,7 +490,7 @@ export type PromptInputProps = Omit< HTMLAttributes, "onSubmit" | "onError" > & { - // e.g., "image/*" or leave undefined for any + // e.g., "image/*", ".pdf" or leave undefined for any accept?: string; multiple?: boolean; // When true, accepts drops anywhere on document. Default false (opt-in). @@ -564,6 +564,10 @@ export const PromptInput = ({ .filter(Boolean); return patterns.some((pattern) => { + if (pattern.startsWith(".")) { + // e.g: .xlsx -> matched against the file name, case-insensitive + return f.name.toLowerCase().endsWith(pattern.toLowerCase()); + } if (pattern.endsWith("/*")) { // e.g: image/* -> image/ const prefix = pattern.slice(0, -1);