diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/dnd-icon.png b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/dnd-icon.png
new file mode 100644
index 00000000..9cb0cd7c
Binary files /dev/null and b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/dnd-icon.png differ
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx
new file mode 100644
index 00000000..d7e4edb6
--- /dev/null
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx
@@ -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 (
+
+
+
+
+
+
Add anything
+
+ Drop your file here to embed it into your
+ workspace auto-magically.
+
+
+
+
+
+ {children(files, setFiles)}
+
+ );
+}
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/Attachments/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/Attachments/index.jsx
new file mode 100644
index 00000000..b0032b95
--- /dev/null
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/Attachments/index.jsx
@@ -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 (
+
+ {attachments.map((attachment) => (
+
+ ))}
+
+ );
+}
+
+/**
+ * @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 (
+
+
+
+
+
+
{file.name}
+
+ {humanFileSize(file.size)}
+
+
+
+ );
+ }
+
+ if (status === "failed") {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {file.name}
+
+
+ {error ?? "this file failed to upload"}. It will not be available
+ in the workspace.
+
+
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
{file.name}
+
File embedded!
+
+
+
+ >
+ );
+}
+
+/**
+ * @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 };
+ }
+}
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx
index fc46fbe9..253f158f 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx
@@ -13,6 +13,7 @@ import AvailableAgentsButton, {
import TextSizeButton from "./TextSizeMenu";
import SpeechToText from "./SpeechToText";
import { Tooltip } from "react-tooltip";
+import AttachmentManager from "./Attachments";
export const PROMPT_INPUT_EVENT = "set_prompt_input";
export default function PromptInput({
@@ -21,6 +22,7 @@ export default function PromptInput({
inputDisabled,
buttonDisabled,
sendCommand,
+ attachments = [],
}) {
const [promptInput, setPromptInput] = useState("");
const { showAgents, setShowAgents } = useAvailableAgents();
@@ -106,10 +108,11 @@ export default function PromptInput({
/>