-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add drag-and-drop to chat window (#1995)
* Add drag-and-drop to chat window * add uploader icon and remove empty space text when attachments are present * color theme * color update
- Loading branch information
1 parent
5e73dce
commit d877d2b
Showing
10 changed files
with
525 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+2.63 KB
frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/dnd-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions
136
frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { useState, useEffect } from "react"; | ||
import { v4 } from "uuid"; | ||
import System from "@/models/system"; | ||
import { useDropzone } from "react-dropzone"; | ||
import DndIcon from "./dnd-icon.png"; | ||
import Workspace from "@/models/workspace"; | ||
import useUser from "@/hooks/useUser"; | ||
|
||
export const REMOVE_ATTACHMENT_EVENT = "ATTACHMENT_REMOVE"; | ||
export const CLEAR_ATTACHMENTS_EVENT = "ATTACHMENT_CLEAR"; | ||
|
||
/** | ||
* File Attachment for automatic upload on the chat container page. | ||
* @typedef Attachment | ||
* @property {string} uid - unique file id. | ||
* @property {File} file - native File object | ||
* @property {('in_progress'|'failed'|'success')} status - the automatic upload status. | ||
* @property {string|null} error - Error message | ||
* @property {{id:string, location:string}|null} document - uploaded document details | ||
*/ | ||
|
||
export default function DnDFileUploaderWrapper({ workspace, children }) { | ||
/** @type {[Attachment[], Function]} */ | ||
const [files, setFiles] = useState([]); | ||
const [ready, setReady] = useState(false); | ||
const [dragging, setDragging] = useState(false); | ||
const { user } = useUser(); | ||
|
||
useEffect(() => { | ||
if (!!user && user.role === "default") return false; | ||
System.checkDocumentProcessorOnline().then((status) => setReady(status)); | ||
}, [user]); | ||
|
||
useEffect(() => { | ||
window.addEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove); | ||
window.addEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments); | ||
|
||
return () => { | ||
window.removeEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove); | ||
window.removeEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments); | ||
}; | ||
}, []); | ||
|
||
/** | ||
* Remove file from uploader queue. | ||
* @param {CustomEvent<{uid: string}>} event | ||
*/ | ||
async function handleRemove(event) { | ||
/** @type {{uid: Attachment['uid'], document: Attachment['document']}} */ | ||
const { uid, document } = event.detail; | ||
setFiles((prev) => prev.filter((prevFile) => prevFile.uid !== uid)); | ||
if (!document.location) return; | ||
await Workspace.deleteAndUnembedFile(workspace.slug, document.location); | ||
} | ||
|
||
/** | ||
* Clear queue of attached files currently in prompt box | ||
*/ | ||
function resetAttachments() { | ||
setFiles([]); | ||
} | ||
|
||
async function onDrop(acceptedFiles, _rejections) { | ||
setDragging(false); | ||
/** @type {Attachment[]} */ | ||
const newAccepted = acceptedFiles.map((file) => { | ||
return { | ||
uid: v4(), | ||
file, | ||
status: "in_progress", | ||
error: null, | ||
}; | ||
}); | ||
setFiles((prev) => [...prev, ...newAccepted]); | ||
|
||
for (const attachment of newAccepted) { | ||
const formData = new FormData(); | ||
formData.append("file", attachment.file, attachment.file.name); | ||
Workspace.uploadAndEmbedFile(workspace.slug, formData).then( | ||
({ response, data }) => { | ||
const updates = { | ||
status: response.ok ? "success" : "failed", | ||
error: data?.error ?? null, | ||
document: data?.document, | ||
}; | ||
|
||
setFiles((prev) => { | ||
return prev.map( | ||
( | ||
/** @type {Attachment} */ | ||
prevFile | ||
) => { | ||
if (prevFile.uid !== attachment.uid) return prevFile; | ||
return { ...prevFile, ...updates }; | ||
} | ||
); | ||
}); | ||
} | ||
); | ||
} | ||
} | ||
|
||
const { getRootProps, getInputProps } = useDropzone({ | ||
onDrop, | ||
disabled: !ready, | ||
noClick: true, | ||
noKeyboard: true, | ||
onDragEnter: () => setDragging(true), | ||
onDragLeave: () => setDragging(false), | ||
}); | ||
|
||
return ( | ||
<div | ||
className={`relative flex flex-col h-full w-full md:mt-0 mt-[40px] p-[1px]`} | ||
{...getRootProps()} | ||
> | ||
<div | ||
hidden={!dragging} | ||
className="absolute top-0 w-full h-full bg-dark-text/90 rounded-2xl border-[4px] border-white z-[9999]" | ||
> | ||
<div className="w-full h-full flex justify-center items-center rounded-xl"> | ||
<div className="flex flex-col gap-y-[14px] justify-center items-center"> | ||
<img src={DndIcon} width={69} height={69} /> | ||
<p className="text-white text-[24px] font-semibold">Add anything</p> | ||
<p className="text-white text-[16px] text-center"> | ||
Drop your file here to embed it into your <br /> | ||
workspace auto-magically. | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
<input {...getInputProps()} /> | ||
{children(files, setFiles)} | ||
</div> | ||
); | ||
} |
176 changes: 176 additions & 0 deletions
176
frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/Attachments/index.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import { | ||
CircleNotch, | ||
FileCode, | ||
FileCsv, | ||
FileDoc, | ||
FileHtml, | ||
FilePdf, | ||
WarningOctagon, | ||
X, | ||
} from "@phosphor-icons/react"; | ||
import { humanFileSize } from "@/utils/numbers"; | ||
import { FileText } from "@phosphor-icons/react/dist/ssr"; | ||
import { REMOVE_ATTACHMENT_EVENT } from "../../DnDWrapper"; | ||
import { Tooltip } from "react-tooltip"; | ||
|
||
/** | ||
* @param {{attachments: import("../../DnDWrapper").Attachment[]}} | ||
* @returns | ||
*/ | ||
export default function AttachmentManager({ attachments }) { | ||
if (attachments.length === 0) return null; | ||
return ( | ||
<div className="flex flex-wrap my-2"> | ||
{attachments.map((attachment) => ( | ||
<AttachmentItem key={attachment.uid} attachment={attachment} /> | ||
))} | ||
</div> | ||
); | ||
} | ||
|
||
/** | ||
* @param {{attachment: import("../../DnDWrapper").Attachment}} | ||
*/ | ||
function AttachmentItem({ attachment }) { | ||
const { uid, file, status, error, document } = attachment; | ||
const { iconBgColor, Icon } = displayFromFile(file); | ||
|
||
function removeFileFromQueue() { | ||
window.dispatchEvent( | ||
new CustomEvent(REMOVE_ATTACHMENT_EVENT, { detail: { uid, document } }) | ||
); | ||
} | ||
|
||
if (status === "in_progress") { | ||
return ( | ||
<div | ||
className={`h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-zinc-800 border border-white/20 w-[200px]`} | ||
> | ||
<div | ||
className={`${iconBgColor} rounded-lg flex items-center justify-center flex-shrink-0 p-1`} | ||
> | ||
<CircleNotch size={30} className="text-white animate-spin" /> | ||
</div> | ||
<div className="flex flex-col w-[130px]"> | ||
<p className="text-white text-xs font-medium truncate">{file.name}</p> | ||
<p className="text-white/60 text-xs font-medium"> | ||
{humanFileSize(file.size)} | ||
</p> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
if (status === "failed") { | ||
return ( | ||
<> | ||
<div | ||
data-tooltip-id={`attachment-uid-${uid}-error`} | ||
data-tooltip-content={error} | ||
className={`relative h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-[#4E140B] border border-transparent w-[200px] group`} | ||
> | ||
<div className="invisible group-hover:visible absolute -top-[5px] -right-[5px] w-fit h-fit z-[10]"> | ||
<button | ||
onClick={removeFileFromQueue} | ||
type="button" | ||
className="bg-zinc-700 hover:bg-red-400 rounded-full p-1 flex items-center justify-center hover:border-transparent border border-white/40" | ||
> | ||
<X | ||
size={10} | ||
className="flex-shrink-0 text-zinc-200 group-hover:text-white" | ||
/> | ||
</button> | ||
</div> | ||
<div | ||
className={`bg-danger rounded-lg flex items-center justify-center flex-shrink-0 p-1`} | ||
> | ||
<WarningOctagon size={30} className="text-white" /> | ||
</div> | ||
<div className="flex flex-col w-[130px]"> | ||
<p className="text-white text-xs font-medium truncate"> | ||
{file.name} | ||
</p> | ||
<p className="text-red-100 text-xs truncate"> | ||
{error ?? "this file failed to upload"}. It will not be available | ||
in the workspace. | ||
</p> | ||
</div> | ||
</div> | ||
<Tooltip | ||
id={`attachment-uid-${uid}-error`} | ||
place="top" | ||
delayShow={300} | ||
className="allm-tooltip !allm-text-xs" | ||
/> | ||
</> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<div | ||
data-tooltip-id={`attachment-uid-${uid}-success`} | ||
data-tooltip-content={`${file.name} was uploaded and embedded into this workspace. It will be available for RAG chat now.`} | ||
className={`relative h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-zinc-800 border border-white/20 w-[200px] group`} | ||
> | ||
<div className="invisible group-hover:visible absolute -top-[5px] -right-[5px] w-fit h-fit z-[10]"> | ||
<button | ||
onClick={removeFileFromQueue} | ||
type="button" | ||
className="bg-zinc-700 hover:bg-red-400 rounded-full p-1 flex items-center justify-center hover:border-transparent border border-white/40" | ||
> | ||
<X | ||
size={10} | ||
className="flex-shrink-0 text-zinc-200 group-hover:text-white" | ||
/> | ||
</button> | ||
</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]"> | ||
<p className="text-white text-xs font-medium truncate">{file.name}</p> | ||
<p className="text-white/80 text-xs font-medium">File embedded!</p> | ||
</div> | ||
</div> | ||
<Tooltip | ||
id={`attachment-uid-${uid}-success`} | ||
place="top" | ||
delayShow={300} | ||
className="allm-tooltip !allm-text-xs" | ||
/> | ||
</> | ||
); | ||
} | ||
|
||
/** | ||
* @param {File} file | ||
* @returns {{iconBgColor:string, Icon: React.Component}} | ||
*/ | ||
function displayFromFile(file) { | ||
const extension = file?.name?.split(".")?.pop()?.toLowerCase() ?? "txt"; | ||
switch (extension) { | ||
case "pdf": | ||
return { iconBgColor: "bg-magenta", Icon: FilePdf }; | ||
case "doc": | ||
case "docx": | ||
return { iconBgColor: "bg-royalblue", Icon: FileDoc }; | ||
case "html": | ||
return { iconBgColor: "bg-warn", Icon: FileHtml }; | ||
case "csv": | ||
case "xlsx": | ||
return { iconBgColor: "bg-success", Icon: FileCsv }; | ||
case "json": | ||
case "sql": | ||
case "js": | ||
case "jsx": | ||
case "cpp": | ||
case "c": | ||
case "c": | ||
return { iconBgColor: "bg-warn", Icon: FileCode }; | ||
default: | ||
return { iconBgColor: "bg-royalblue", Icon: FileText }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.