mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-15 02:50:10 +01:00
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
This commit is contained in:
parent
5e73dce506
commit
d877d2b7ad
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -39,11 +39,13 @@
|
|||||||
"openrouter",
|
"openrouter",
|
||||||
"pagerender",
|
"pagerender",
|
||||||
"Qdrant",
|
"Qdrant",
|
||||||
|
"royalblue",
|
||||||
"searxng",
|
"searxng",
|
||||||
"Serper",
|
"Serper",
|
||||||
"Serply",
|
"Serply",
|
||||||
"textgenwebui",
|
"textgenwebui",
|
||||||
"togetherai",
|
"togetherai",
|
||||||
|
"Unembed",
|
||||||
"vectordbs",
|
"vectordbs",
|
||||||
"Weaviate",
|
"Weaviate",
|
||||||
"Zilliz"
|
"Zilliz"
|
||||||
|
@ -17,6 +17,7 @@ export default function ChatHistory({
|
|||||||
sendCommand,
|
sendCommand,
|
||||||
updateHistory,
|
updateHistory,
|
||||||
regenerateAssistantMessage,
|
regenerateAssistantMessage,
|
||||||
|
hasAttachments = false,
|
||||||
}) {
|
}) {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { threadSlug = null } = useParams();
|
const { threadSlug = null } = useParams();
|
||||||
@ -144,7 +145,7 @@ export default function ChatHistory({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (history.length === 0) {
|
if (history.length === 0 && !hasAttachments) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full md:mt-0 pb-44 md:pb-40 w-full justify-end items-center">
|
<div className="flex flex-col h-full md:mt-0 pb-44 md:pb-40 w-full justify-end items-center">
|
||||||
<div className="flex flex-col items-center md:items-start md:max-w-[600px] w-full px-4">
|
<div className="flex flex-col items-center md:items-start md:max-w-[600px] w-full px-4">
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ import AvailableAgentsButton, {
|
|||||||
import TextSizeButton from "./TextSizeMenu";
|
import TextSizeButton from "./TextSizeMenu";
|
||||||
import SpeechToText from "./SpeechToText";
|
import SpeechToText from "./SpeechToText";
|
||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
|
import AttachmentManager from "./Attachments";
|
||||||
|
|
||||||
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
||||||
export default function PromptInput({
|
export default function PromptInput({
|
||||||
@ -21,6 +22,7 @@ export default function PromptInput({
|
|||||||
inputDisabled,
|
inputDisabled,
|
||||||
buttonDisabled,
|
buttonDisabled,
|
||||||
sendCommand,
|
sendCommand,
|
||||||
|
attachments = [],
|
||||||
}) {
|
}) {
|
||||||
const [promptInput, setPromptInput] = useState("");
|
const [promptInput, setPromptInput] = useState("");
|
||||||
const { showAgents, setShowAgents } = useAvailableAgents();
|
const { showAgents, setShowAgents } = useAvailableAgents();
|
||||||
@ -106,10 +108,11 @@ export default function PromptInput({
|
|||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl"
|
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl items-center"
|
||||||
>
|
>
|
||||||
<div className="flex items-center rounded-lg md:mb-4">
|
<div className="flex items-center rounded-lg md:mb-4">
|
||||||
<div className="w-[600px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
|
<div className="w-[635px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
|
||||||
|
<AttachmentManager attachments={attachments} />
|
||||||
<div className="flex items-center w-full border-b-2 border-gray-500/50">
|
<div className="flex items-center w-full border-b-2 border-gray-500/50">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import ChatHistory from "./ChatHistory";
|
import ChatHistory from "./ChatHistory";
|
||||||
|
import DnDFileUploadWrapper, { CLEAR_ATTACHMENTS_EVENT } from "./DnDWrapper";
|
||||||
import PromptInput, { PROMPT_INPUT_EVENT } from "./PromptInput";
|
import PromptInput, { PROMPT_INPUT_EVENT } from "./PromptInput";
|
||||||
import Workspace from "@/models/workspace";
|
import Workspace from "@/models/workspace";
|
||||||
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
|
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
|
||||||
@ -121,13 +122,13 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Simplify this
|
|
||||||
if (!promptMessage || !promptMessage?.userMessage) return false;
|
if (!promptMessage || !promptMessage?.userMessage) return false;
|
||||||
if (!!threadSlug) {
|
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
|
||||||
await Workspace.threads.streamChat(
|
await Workspace.multiplexStream({
|
||||||
{ workspaceSlug: workspace.slug, threadSlug },
|
workspaceSlug: workspace.slug,
|
||||||
promptMessage.userMessage,
|
threadSlug,
|
||||||
(chatResult) =>
|
prompt: promptMessage.userMessage,
|
||||||
|
chatHandler: (chatResult) =>
|
||||||
handleChat(
|
handleChat(
|
||||||
chatResult,
|
chatResult,
|
||||||
setLoadingResponse,
|
setLoadingResponse,
|
||||||
@ -135,23 +136,8 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
remHistory,
|
remHistory,
|
||||||
_chatHistory,
|
_chatHistory,
|
||||||
setSocketId
|
setSocketId
|
||||||
)
|
),
|
||||||
);
|
});
|
||||||
} else {
|
|
||||||
await Workspace.streamChat(
|
|
||||||
workspace,
|
|
||||||
promptMessage.userMessage,
|
|
||||||
(chatResult) =>
|
|
||||||
handleChat(
|
|
||||||
chatResult,
|
|
||||||
setLoadingResponse,
|
|
||||||
setChatHistory,
|
|
||||||
remHistory,
|
|
||||||
_chatHistory,
|
|
||||||
setSocketId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadingResponse === true && fetchReply();
|
loadingResponse === true && fetchReply();
|
||||||
@ -205,6 +191,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
});
|
});
|
||||||
setWebsocket(socket);
|
setWebsocket(socket);
|
||||||
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
|
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
|
||||||
|
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setChatHistory((prev) => [
|
setChatHistory((prev) => [
|
||||||
...prev.filter((msg) => !!msg.content),
|
...prev.filter((msg) => !!msg.content),
|
||||||
@ -234,13 +221,16 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col h-full w-full md:mt-0 mt-[40px]">
|
<DnDFileUploadWrapper workspace={workspace}>
|
||||||
|
{(files) => (
|
||||||
|
<>
|
||||||
<ChatHistory
|
<ChatHistory
|
||||||
history={chatHistory}
|
history={chatHistory}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
sendCommand={sendCommand}
|
sendCommand={sendCommand}
|
||||||
updateHistory={setChatHistory}
|
updateHistory={setChatHistory}
|
||||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||||
|
hasAttachments={files.length > 0}
|
||||||
/>
|
/>
|
||||||
<PromptInput
|
<PromptInput
|
||||||
submit={handleSubmit}
|
submit={handleSubmit}
|
||||||
@ -248,8 +238,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
inputDisabled={loadingResponse}
|
inputDisabled={loadingResponse}
|
||||||
buttonDisabled={loadingResponse}
|
buttonDisabled={loadingResponse}
|
||||||
sendCommand={sendCommand}
|
sendCommand={sendCommand}
|
||||||
|
attachments={files}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</DnDFileUploadWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,20 @@ const Workspace = {
|
|||||||
);
|
);
|
||||||
return this._updateChatResponse(slug, chatId, newText);
|
return this._updateChatResponse(slug, chatId, newText);
|
||||||
},
|
},
|
||||||
|
multiplexStream: async function ({
|
||||||
|
workspaceSlug,
|
||||||
|
threadSlug = null,
|
||||||
|
prompt,
|
||||||
|
chatHandler,
|
||||||
|
}) {
|
||||||
|
if (!!threadSlug)
|
||||||
|
return this.threads.streamChat(
|
||||||
|
{ workspaceSlug, threadSlug },
|
||||||
|
prompt,
|
||||||
|
chatHandler
|
||||||
|
);
|
||||||
|
return this.streamChat({ slug: workspaceSlug }, prompt, chatHandler);
|
||||||
|
},
|
||||||
streamChat: async function ({ slug }, message, handleChat) {
|
streamChat: async function ({ slug }, message, handleChat) {
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
|
|
||||||
@ -411,6 +425,43 @@ const Workspace = {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Uploads and embeds a single file in a single call into a workspace
|
||||||
|
* @param {string} slug - workspace slug
|
||||||
|
* @param {FormData} formData
|
||||||
|
* @returns {Promise<{response: {ok: boolean}, data: {success: boolean, error: string|null, document: {id: string, location:string}|null}}>}
|
||||||
|
*/
|
||||||
|
uploadAndEmbedFile: async function (slug, formData) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/workspace/${slug}/upload-and-embed`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
headers: baseHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { response, data };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes and un-embeds a single file in a single call from a workspace
|
||||||
|
* @param {string} slug - workspace slug
|
||||||
|
* @param {string} documentLocation - location of file eg: custom-documents/my-file-uuid.json
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
deleteAndUnembedFile: async function (slug, documentLocation) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/workspace/${slug}/remove-and-unembed`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify({ documentLocation }),
|
||||||
|
headers: baseHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
},
|
||||||
threads: WorkspaceThread,
|
threads: WorkspaceThread,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +36,11 @@ export default {
|
|||||||
"dark-text": "#222628",
|
"dark-text": "#222628",
|
||||||
description: "#D2D5DB",
|
description: "#D2D5DB",
|
||||||
"x-button": "#9CA3AF",
|
"x-button": "#9CA3AF",
|
||||||
|
royalblue: '#3538CD',
|
||||||
|
magenta: '#C11574',
|
||||||
|
danger: '#F04438',
|
||||||
|
warn: '#854708',
|
||||||
|
success: '#027A48',
|
||||||
darker: "#F4F4F4"
|
darker: "#F4F4F4"
|
||||||
},
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
|
@ -33,6 +33,7 @@ const {
|
|||||||
const { getTTSProvider } = require("../utils/TextToSpeech");
|
const { getTTSProvider } = require("../utils/TextToSpeech");
|
||||||
const { WorkspaceThread } = require("../models/workspaceThread");
|
const { WorkspaceThread } = require("../models/workspaceThread");
|
||||||
const truncate = require("truncate");
|
const truncate = require("truncate");
|
||||||
|
const { purgeDocument } = require("../utils/files/purgeDocument");
|
||||||
|
|
||||||
function workspaceEndpoints(app) {
|
function workspaceEndpoints(app) {
|
||||||
if (!app) return;
|
if (!app) return;
|
||||||
@ -863,6 +864,114 @@ function workspaceEndpoints(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */
|
||||||
|
app.post(
|
||||||
|
"/workspace/:slug/upload-and-embed",
|
||||||
|
[
|
||||||
|
validatedRequest,
|
||||||
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
||||||
|
handleFileUpload,
|
||||||
|
],
|
||||||
|
async function (request, response) {
|
||||||
|
try {
|
||||||
|
const { slug = null } = request.params;
|
||||||
|
const user = await userFromSession(request, response);
|
||||||
|
const currWorkspace = multiUserMode(response)
|
||||||
|
? await Workspace.getWithUser(user, { slug })
|
||||||
|
: await Workspace.get({ slug });
|
||||||
|
|
||||||
|
if (!currWorkspace) {
|
||||||
|
response.sendStatus(400).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Collector = new CollectorApi();
|
||||||
|
const { originalname } = request.file;
|
||||||
|
const processingOnline = await Collector.online();
|
||||||
|
|
||||||
|
if (!processingOnline) {
|
||||||
|
response
|
||||||
|
.status(500)
|
||||||
|
.json({
|
||||||
|
success: false,
|
||||||
|
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
|
||||||
|
})
|
||||||
|
.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, reason, documents } =
|
||||||
|
await Collector.processDocument(originalname);
|
||||||
|
if (!success || documents?.length === 0) {
|
||||||
|
response.status(500).json({ success: false, error: reason }).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collector.log(
|
||||||
|
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
|
||||||
|
);
|
||||||
|
await Telemetry.sendTelemetry("document_uploaded");
|
||||||
|
await EventLogs.logEvent(
|
||||||
|
"document_uploaded",
|
||||||
|
{
|
||||||
|
documentName: originalname,
|
||||||
|
},
|
||||||
|
response.locals?.user?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const document = documents[0];
|
||||||
|
const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
|
||||||
|
currWorkspace,
|
||||||
|
[document.location],
|
||||||
|
response.locals?.user?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failedToEmbed.length > 0)
|
||||||
|
return response
|
||||||
|
.status(200)
|
||||||
|
.json({ success: false, error: errors?.[0], document: null });
|
||||||
|
|
||||||
|
response.status(200).json({
|
||||||
|
success: true,
|
||||||
|
error: null,
|
||||||
|
document: { id: document.id, location: document.location },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message, e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.delete(
|
||||||
|
"/workspace/:slug/remove-and-unembed",
|
||||||
|
[
|
||||||
|
validatedRequest,
|
||||||
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
||||||
|
handleFileUpload,
|
||||||
|
],
|
||||||
|
async function (request, response) {
|
||||||
|
try {
|
||||||
|
const { slug = null } = request.params;
|
||||||
|
const body = reqBody(request);
|
||||||
|
const user = await userFromSession(request, response);
|
||||||
|
const currWorkspace = multiUserMode(response)
|
||||||
|
? await Workspace.getWithUser(user, { slug })
|
||||||
|
: await Workspace.get({ slug });
|
||||||
|
|
||||||
|
if (!currWorkspace || !body.documentLocation)
|
||||||
|
return response.sendStatus(400).end();
|
||||||
|
|
||||||
|
// Will delete the document from the entire system + wil unembed it.
|
||||||
|
await purgeDocument(body.documentLocation);
|
||||||
|
response.status(200).end();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message, e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { workspaceEndpoints };
|
module.exports = { workspaceEndpoints };
|
||||||
|
Loading…
Reference in New Issue
Block a user