Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/prompt-input-accept-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ai-elements": patch
---

Match file extension patterns such as `.pdf` in the PromptInput `accept` prop
2 changes: 1 addition & 1 deletion apps/docs/content/components/(chatbot)/prompt-input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
67 changes: 67 additions & 0 deletions packages/elements/__tests__/prompt-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<button
data-testid="add-sheet"
onClick={() => attachments.add([sheetFile])}
type="button"
>
Add Sheet
</button>
<button
data-testid="add-csv"
onClick={() => attachments.add([csvFile])}
type="button"
>
Add CSV
</button>
<button
data-testid="add-text"
onClick={() => attachments.add([textFile])}
type="button"
>
Add Text
</button>
<div data-testid="count">{attachments.files.length}</div>
</>
);
};

render(
<PromptInput accept=".xlsx, .csv" onError={onError} onSubmit={onSubmit}>
<PromptInputBody>
<AttachmentConsumer />
<PromptInputTextarea />
</PromptInputBody>
</PromptInput>
);

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();
Expand Down
6 changes: 5 additions & 1 deletion packages/elements/src/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ export type PromptInputProps = Omit<
HTMLAttributes<HTMLFormElement>,
"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).
Expand Down Expand Up @@ -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);
Expand Down