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 DndUploaderContext = createContext();
|
||||||
export const REMOVE_ATTACHMENT_EVENT = "ATTACHMENT_REMOVE";
|
export const REMOVE_ATTACHMENT_EVENT = "ATTACHMENT_REMOVE";
|
||||||
export const CLEAR_ATTACHMENTS_EVENT = "ATTACHMENT_CLEAR";
|
export const CLEAR_ATTACHMENTS_EVENT = "ATTACHMENT_CLEAR";
|
||||||
|
export const PASTE_ATTACHMENT_EVENT = "ATTACHMENT_PASTED";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File Attachment for automatic upload on the chat container page.
|
* File Attachment for automatic upload on the chat container page.
|
||||||
@ -36,10 +37,15 @@ export function DnDFileUploaderProvider({ workspace, children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);
|
window.addEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);
|
||||||
window.addEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);
|
window.addEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);
|
||||||
|
window.addEventListener(PASTE_ATTACHMENT_EVENT, handlePastedAttachment);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);
|
window.removeEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);
|
||||||
window.removeEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);
|
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.
|
* Handle dropped files.
|
||||||
* @param {Attachment[]} acceptedFiles
|
* @param {Attachment[]} acceptedFiles
|
||||||
@ -119,8 +158,15 @@ export function DnDFileUploaderProvider({ workspace, children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFiles((prev) => [...prev, ...newAccepted]);
|
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.
|
// Images/attachments are chat specific.
|
||||||
if (attachment.type === "attachment") continue;
|
if (attachment.type === "attachment") continue;
|
||||||
|
|
||||||
@ -200,7 +246,7 @@ export default function DnDFileUploaderWrapper({ children }) {
|
|||||||
/**
|
/**
|
||||||
* Convert image types into Base64 strings for requests.
|
* Convert image types into Base64 strings for requests.
|
||||||
* @param {File} file
|
* @param {File} file
|
||||||
* @returns {string}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async function toBase64(file) {
|
async function toBase64(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -33,7 +33,8 @@ export default function AttachmentManager({ attachments }) {
|
|||||||
* @param {{attachment: import("../../DnDWrapper").Attachment}}
|
* @param {{attachment: import("../../DnDWrapper").Attachment}}
|
||||||
*/
|
*/
|
||||||
function AttachmentItem({ 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);
|
const { iconBgColor, Icon } = displayFromFile(file);
|
||||||
|
|
||||||
function removeFileFromQueue() {
|
function removeFileFromQueue() {
|
||||||
@ -127,11 +128,18 @@ function AttachmentItem({ attachment }) {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{contentString ? (
|
||||||
className={`${iconBgColor} rounded-lg flex items-center justify-center flex-shrink-0 p-1`}
|
<img
|
||||||
>
|
src={contentString}
|
||||||
<Icon size={30} className="text-white" />
|
className={`${iconBgColor} w-[30px] h-[30px] rounded-lg flex items-center justify-center`}
|
||||||
</div>
|
/>
|
||||||
|
) : (
|
||||||
|
<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]">
|
<div className="flex flex-col w-[130px]">
|
||||||
<p className="text-white text-xs font-medium truncate">
|
<p className="text-white text-xs font-medium truncate">
|
||||||
{file.name}
|
{file.name}
|
||||||
|
@ -15,6 +15,7 @@ import SpeechToText from "./SpeechToText";
|
|||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
import AttachmentManager from "./Attachments";
|
import AttachmentManager from "./Attachments";
|
||||||
import AttachItem from "./AttachItem";
|
import AttachItem from "./AttachItem";
|
||||||
|
import { PASTE_ATTACHMENT_EVENT } from "../DnDWrapper";
|
||||||
|
|
||||||
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
||||||
export default function PromptInput({
|
export default function PromptInput({
|
||||||
@ -91,6 +92,39 @@ export default function PromptInput({
|
|||||||
element.style.height = `${element.scrollHeight}px`;
|
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 watchForSlash = debounce(checkForSlash, 300);
|
||||||
const watchForAt = debounce(checkForAt, 300);
|
const watchForAt = debounce(checkForAt, 300);
|
||||||
|
|
||||||
@ -125,6 +159,7 @@ export default function PromptInput({
|
|||||||
setPromptInput(e.target.value);
|
setPromptInput(e.target.value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={captureEnter}
|
onKeyDown={captureEnter}
|
||||||
|
onPaste={handlePasteEvent}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={inputDisabled}
|
disabled={inputDisabled}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setFocused(true)}
|
||||||
|
Loading…
Reference in New Issue
Block a user