Skip to content

Commit

Permalink
Add ability to copy/paste images, files, and text from web, local, or…
Browse files Browse the repository at this point in the history
… otherwise (#2326)
  • Loading branch information
timothycarambat authored Sep 19, 2024
1 parent 4fa3d6d commit 84c1f6e
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useUser from "@/hooks/useUser";
export const DndUploaderContext = createContext();
export const REMOVE_ATTACHMENT_EVENT = "ATTACHMENT_REMOVE";
export const CLEAR_ATTACHMENTS_EVENT = "ATTACHMENT_CLEAR";
export const PASTE_ATTACHMENT_EVENT = "ATTACHMENT_PASTED";

/**
* File Attachment for automatic upload on the chat container page.
Expand Down Expand Up @@ -36,10 +37,15 @@ export function DnDFileUploaderProvider({ workspace, children }) {
useEffect(() => {
window.addEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);
window.addEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);
window.addEventListener(PASTE_ATTACHMENT_EVENT, handlePastedAttachment);

return () => {
window.removeEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);
window.removeEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);
window.removeEventListener(
PASTE_ATTACHMENT_EVENT,
handlePastedAttachment
);
};
}, []);

Expand Down Expand Up @@ -86,6 +92,39 @@ export function DnDFileUploaderProvider({ workspace, children }) {
);
}

/**
* Handle pasted attachments.
* @param {CustomEvent<{files: File[]}>} event
*/
async function handlePastedAttachment(event) {
const { files = [] } = event.detail;
if (!files.length) return;
const newAccepted = [];
for (const file of files) {
if (file.type.startsWith("image/")) {
newAccepted.push({
uid: v4(),
file,
contentString: await toBase64(file),
status: "success",
error: null,
type: "attachment",
});
} else {
newAccepted.push({
uid: v4(),
file,
contentString: null,
status: "in_progress",
error: null,
type: "upload",
});
}
}
setFiles((prev) => [...prev, ...newAccepted]);
embedEligibleAttachments(newAccepted);
}

/**
* Handle dropped files.
* @param {Attachment[]} acceptedFiles
Expand Down Expand Up @@ -119,8 +158,15 @@ export function DnDFileUploaderProvider({ workspace, children }) {
}

setFiles((prev) => [...prev, ...newAccepted]);
embedEligibleAttachments(newAccepted);
}

for (const attachment of newAccepted) {
/**
* Embeds attachments that are eligible for embedding - basically files that are not images.
* @param {Attachment[]} newAttachments
*/
function embedEligibleAttachments(newAttachments = []) {
for (const attachment of newAttachments) {
// Images/attachments are chat specific.
if (attachment.type === "attachment") continue;

Expand Down Expand Up @@ -200,7 +246,7 @@ export default function DnDFileUploaderWrapper({ children }) {
/**
* Convert image types into Base64 strings for requests.
* @param {File} file
* @returns {string}
* @returns {Promise<string>}
*/
async function toBase64(file) {
return new Promise((resolve, reject) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export default function AttachmentManager({ attachments }) {
* @param {{attachment: import("../../DnDWrapper").Attachment}}
*/
function AttachmentItem({ attachment }) {
const { uid, file, status, error, document, type } = attachment;
const { uid, file, status, error, document, type, contentString } =
attachment;
const { iconBgColor, Icon } = displayFromFile(file);

function removeFileFromQueue() {
Expand Down Expand Up @@ -127,11 +128,18 @@ function AttachmentItem({ attachment }) {
/>
</button>
</div>
<div
className={`${iconBgColor} rounded-lg flex items-center justify-center flex-shrink-0 p-1`}
>
<Icon size={30} className="text-white" />
</div>
{contentString ? (
<img
src={contentString}
className={`${iconBgColor} w-[30px] h-[30px] rounded-lg flex items-center justify-center`}
/>
) : (
<div
className={`${iconBgColor} rounded-lg flex items-center justify-center flex-shrink-0 p-1`}
>
<Icon size={30} className="text-white" />
</div>
)}
<div className="flex flex-col w-[130px]">
<p className="text-white text-xs font-medium truncate">
{file.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SpeechToText from "./SpeechToText";
import { Tooltip } from "react-tooltip";
import AttachmentManager from "./Attachments";
import AttachItem from "./AttachItem";
import { PASTE_ATTACHMENT_EVENT } from "../DnDWrapper";

export const PROMPT_INPUT_EVENT = "set_prompt_input";
export default function PromptInput({
Expand Down Expand Up @@ -91,6 +92,39 @@ export default function PromptInput({
element.style.height = `${element.scrollHeight}px`;
};

const handlePasteEvent = (e) => {
e.preventDefault();
if (e.clipboardData.items.length === 0) return false;

// paste any clipboard items that are images.
for (const item of e.clipboardData.items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
window.dispatchEvent(
new CustomEvent(PASTE_ATTACHMENT_EVENT, {
detail: { files: [file] },
})
);
continue;
}

// handle files specifically that are not images as uploads
if (item.kind === "file") {
const file = item.getAsFile();
window.dispatchEvent(
new CustomEvent(PASTE_ATTACHMENT_EVENT, {
detail: { files: [file] },
})
);
continue;
}
}

const pasteText = e.clipboardData.getData("text/plain");
if (pasteText) setPromptInput(pasteText.trim());
return;
};

const watchForSlash = debounce(checkForSlash, 300);
const watchForAt = debounce(checkForAt, 300);

Expand Down Expand Up @@ -125,6 +159,7 @@ export default function PromptInput({
setPromptInput(e.target.value);
}}
onKeyDown={captureEnter}
onPaste={handlePasteEvent}
required={true}
disabled={inputDisabled}
onFocus={() => setFocused(true)}
Expand Down

0 comments on commit 84c1f6e

Please sign in to comment.