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-report-partial-rejections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ai-elements": patch
---

Call PromptInput onError when only some of the added files fail accept or maxFileSize
3 changes: 2 additions & 1 deletion apps/docs/content/components/(chatbot)/prompt-input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@ Buttons can display tooltips with optional keyboard shortcut hints. Hover over t
type: "number",
},
onError: {
description: "Handler for file validation errors.",
description:
"Handler for file validation errors. Also called when only some of the added files are rejected; the rest are still added.",
type: '(err: { code: "max_files" | "max_file_size" | "accept", message: string }) => void',
},
"...props": {
Expand Down
93 changes: 93 additions & 0 deletions packages/elements/__tests__/prompt-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,99 @@ describe("file validation", () => {
});
});

describe("partially rejected files", () => {
const renderWithAttachments = (
props: Partial<React.ComponentProps<typeof PromptInput>>,
filesToAdd: File[]
) => {
const AttachmentConsumer = () => {
const attachments = usePromptInputAttachments();
return (
<>
<button
data-testid="add-files"
onClick={() => attachments.add(filesToAdd)}
type="button"
>
Add Files
</button>
<div data-testid="count">{attachments.files.length}</div>
</>
);
};

return (
<PromptInput onSubmit={vi.fn()} {...props}>
<PromptInputBody>
<AttachmentConsumer />
<PromptInputTextarea />
</PromptInputBody>
</PromptInput>
);
};

it("reports files that do not match accept and adds the rest", async () => {
setupPromptInputTests();
const onError = vi.fn();
const user = userEvent.setup();
const image = new File(["image"], "photo.png", { type: "image/png" });
const text = new File(["text"], "notes.txt", { type: "text/plain" });

render(
renderWithAttachments({ accept: "image/*", onError }, [image, text])
);
await user.click(screen.getByTestId("add-files"));

expect(screen.getByTestId("count")).toHaveTextContent("1");
expect(onError).toHaveBeenCalledWith({
code: "accept",
message: expect.any(String),
});
});

it("reports files over maxFileSize and adds the rest", async () => {
setupPromptInputTests();
const onError = vi.fn();
const user = userEvent.setup();
const small = new File(["small"], "small.txt", { type: "text/plain" });
const large = new File(["large"], "large.txt", { type: "text/plain" });
Object.defineProperty(large, "size", { value: 2000 });

render(
renderWithAttachments({ maxFileSize: 1000, onError }, [small, large])
);
await user.click(screen.getByTestId("add-files"));

expect(screen.getByTestId("count")).toHaveTextContent("1");
expect(onError).toHaveBeenCalledWith({
code: "max_file_size",
message: expect.any(String),
});
});

it("reports partially rejected files inside PromptInputProvider", async () => {
setupPromptInputTests();
const onError = vi.fn();
const user = userEvent.setup();
const { PromptInputProvider } = await import("../src/prompt-input");
const image = new File(["image"], "photo.png", { type: "image/png" });
const text = new File(["text"], "notes.txt", { type: "text/plain" });

render(
<PromptInputProvider>
{renderWithAttachments({ accept: "image/*", onError }, [image, text])}
</PromptInputProvider>
);
await user.click(screen.getByTestId("add-files"));

expect(screen.getByTestId("count")).toHaveTextContent("1");
expect(onError).toHaveBeenCalledWith({
code: "accept",
message: expect.any(String),
});
});
});

describe("drag and drop", () => {
it("renders with globalDrop prop", () => {
setupPromptInputTests();
Expand Down
26 changes: 26 additions & 0 deletions packages/elements/src/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,13 @@ export const PromptInput = ({
});
return;
}
if (accepted.length < incoming.length) {
onError?.({
code: "accept",
message:
"Some files do not match the accepted types and were not added.",
});
}
const withinSize = (f: File) =>
maxFileSize ? f.size <= maxFileSize : true;
const sized = accepted.filter(withinSize);
Expand All @@ -596,6 +603,12 @@ export const PromptInput = ({
});
return;
}
if (sized.length < accepted.length) {
onError?.({
code: "max_file_size",
message: "Some files exceed the maximum size and were not added.",
});
}

setItems((prev) => {
const capacity =
Expand Down Expand Up @@ -650,6 +663,13 @@ export const PromptInput = ({
});
return;
}
if (accepted.length < incoming.length) {
onError?.({
code: "accept",
message:
"Some files do not match the accepted types and were not added.",
});
}
const withinSize = (f: File) =>
maxFileSize ? f.size <= maxFileSize : true;
const sized = accepted.filter(withinSize);
Expand All @@ -660,6 +680,12 @@ export const PromptInput = ({
});
return;
}
if (sized.length < accepted.length) {
onError?.({
code: "max_file_size",
message: "Some files exceed the maximum size and were not added.",
});
}

const currentCount = files.length;
const capacity =
Expand Down