mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 12:40:09 +01:00
Add ability to copy/paste images, files, and text from web, local, or otherwise (#2326)
This commit is contained in:
parent
4fa3d6d333
commit
84c1f6e0ea
@ -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.
|
||||
@ -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
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
@ -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() {
|
||||
@ -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}
|
||||
|
@ -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({
|
||||
@ -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);
|
||||
|
||||
@ -125,6 +159,7 @@ export default function PromptInput({
|
||||
setPromptInput(e.target.value);
|
||||
}}
|
||||
onKeyDown={captureEnter}
|
||||
onPaste={handlePasteEvent}
|
||||
required={true}
|
||||
disabled={inputDisabled}
|
||||
onFocus={() => setFocused(true)}
|
||||
|
Loading…
Reference in New Issue
Block a user