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-paste-text-with-image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ai-elements": patch
---

Paste the text of a spreadsheet or document selection in PromptInput instead of attaching its image
1 change: 1 addition & 0 deletions apps/docs/content/components/(chatbot)/prompt-input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export async function POST(req: Request) {

- Auto-resizing textarea that adjusts height based on content
- File attachment support with drag-and-drop
- Paste files from the clipboard; a spreadsheet or document selection pastes as text
- Built-in screenshot capture action
- Image preview for image attachments
- Configurable file constraints (max files, max size, accepted types)
Expand Down
91 changes: 91 additions & 0 deletions packages/elements/__tests__/prompt-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,7 @@ describe("paste functionality", () => {

// Mock clipboardData items
pasteEvent.clipboardData = {
getData: () => "",
items: [
{
getAsFile: () => file,
Expand All @@ -2014,6 +2015,96 @@ describe("paste functionality", () => {
});
});

it("pastes the text of a spreadsheet selection instead of its image", async () => {
setupPromptInputTests();
const onSubmit = vi.fn();

const AttachmentConsumer = () => {
const attachments = usePromptInputAttachments();
return <div data-testid="count">{attachments.files.length}</div>;
};

render(
<PromptInput onSubmit={onSubmit}>
<PromptInputBody>
<AttachmentConsumer />
<PromptInputTextarea />
</PromptInputBody>
</PromptInput>
);

const textarea = screen.getByPlaceholderText(
"What would you like to know?"
);
textarea.focus();

const clipboardData = new DataTransfer();
clipboardData.setData("text/plain", "Q1\t42");
clipboardData.setData(
"text/html",
"<table><tr><td>Q1</td><td>42</td></tr></table>"
);
clipboardData.items.add(
new File(["image"], "image.png", { type: "image/png" })
);
const pasteEvent = new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData,
});

await act(() => {
textarea.dispatchEvent(pasteEvent);
});

expect(pasteEvent.defaultPrevented).toBeFalsy();
expect(screen.getByTestId("count")).toHaveTextContent("0");
});

it("attaches a copied file whose name is also on the clipboard as text", async () => {
setupPromptInputTests();
const onSubmit = vi.fn();

const AttachmentConsumer = () => {
const attachments = usePromptInputAttachments();
return <div data-testid="count">{attachments.files.length}</div>;
};

render(
<PromptInput onSubmit={onSubmit}>
<PromptInputBody>
<AttachmentConsumer />
<PromptInputTextarea />
</PromptInputBody>
</PromptInput>
);

const textarea = screen.getByPlaceholderText(
"What would you like to know?"
);
textarea.focus();

const clipboardData = new DataTransfer();
clipboardData.setData("text/plain", "report.png");
clipboardData.items.add(
new File(["image"], "report.png", { type: "image/png" })
);
const pasteEvent = new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData,
});

await act(() => {
textarea.dispatchEvent(pasteEvent);
});

await vi.waitFor(() => {
expect(screen.getByTestId("count")).toHaveTextContent("1");
});
expect(pasteEvent.defaultPrevented).toBeTruthy();
});

it("handles paste with no files", () => {
setupPromptInputTests();
const onSubmit = vi.fn();
Expand Down
8 changes: 8 additions & 0 deletions packages/elements/src/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,14 @@ export const PromptInputTextarea = ({
return;
}

// Copied spreadsheet cells also carry a picture of the selection; paste their text instead
if (
event.clipboardData.getData("text/plain") &&
event.clipboardData.getData("text/html")
) {
return;
}

const files: File[] = [];

for (const item of items) {
Expand Down