{
- e.preventDefault();
-
- // Get the plain text.
- const text = e.clipboardData.getData("text/plain");
-
- const selection = window.getSelection();
- if (!selection) {
- return;
- }
- const range = selection.getRangeAt(0);
- let node = range.endContainer;
- let offset = range.endOffset;
-
- if (
- // @ts-expect-error - parentNode is the contenteditable, it has a getAttribute.
- node.getAttribute &&
- // @ts-expect-error - parentNode is the contenteditable, it has a getAttribute.
- node.getAttribute("id") === "dust-input-bar"
- ) {
- const textNode = document.createTextNode("");
- node.appendChild(textNode);
- node = textNode;
- offset = 0;
- }
-
- if (
- node.parentNode &&
- // @ts-expect-error - parentNode is the contenteditable, it has a getAttribute.
- node.parentNode.getAttribute &&
- // @ts-expect-error - parentNode is the contenteditable, it has a getAttribute.
- node.parentNode.getAttribute("id") === "dust-input-bar"
- ) {
- // Inject the text at the cursor position.
- node.textContent =
- node.textContent?.slice(0, offset) +
- text +
- node.textContent?.slice(offset);
- }
-
- // Scroll to the end of the input
- if (inputRef.current) {
- setTimeout(() => {
- const element = inputRef.current;
- if (element) {
- element.scrollTop = element.scrollHeight;
- }
- }, 0);
- }
-
- // Move the cursor to the end of the paste.
- const newRange = document.createRange();
- newRange.setStart(node, offset + text.length);
- newRange.setEnd(node, offset + text.length);
- selection.removeAllRanges();
- selection.addRange(newRange);
- }}
- onKeyDown={(e) => {
- // We prevent the content editable from creating italics, bold and underline.
- if (e.ctrlKey || e.metaKey) {
- if (e.key === "u" || e.key === "b" || e.key === "i") {
- e.preventDefault();
- }
- }
- if (!e.shiftKey && e.key === "Enter") {
- e.preventDefault();
- e.stopPropagation();
- void handleSubmit();
- }
- }}
- onInput={() => {
- const selection = window.getSelection();
- if (
- selection &&
- selection.rangeCount !== 0 &&
- selection.isCollapsed
- ) {
- const range = selection.getRangeAt(0);
- const node = range.endContainer;
- const offset = range.endOffset;
-
- const lastOne = node.textContent
- ? node.textContent.slice(offset - 1, offset)
- : null;
- const preLastOne = node.textContent
- ? node.textContent.slice(offset - 2, offset - 1)
- : null;
-
- // Mention selection logic.
-
- if (
- lastOne === "@" &&
- (preLastOne === " " || preLastOne === "") &&
- node.textContent &&
- node.parentNode &&
- // @ts-expect-error - parentNode is the contenteditable, it has a getAttribute.
- node.parentNode.getAttribute &&
- // @ts-expect-error - parentNode is the contenteditable, it has a getAttribute.
- node.parentNode.getAttribute("id") === "dust-input-bar"
- ) {
- const mentionSelectNode = document.createElement("div");
-
- mentionSelectNode.style.display = "inline-block";
- mentionSelectNode.setAttribute("key", "mentionSelect");
- mentionSelectNode.className = "text-brand font-medium";
- mentionSelectNode.textContent = "@";
- mentionSelectNode.contentEditable = "false";
-
- const inputNode = document.createElement("span");
- inputNode.setAttribute("ignore", "none");
- inputNode.className = classNames(
- "min-w-0 px-0 py-0",
- "border-none outline-none focus:outline-none focus:border-none ring-0 focus:ring-0",
- "text-brand font-medium"
- );
- inputNode.contentEditable = "true";
-
- mentionSelectNode.appendChild(inputNode);
-
- const beforeTextNode = document.createTextNode(
- node.textContent.slice(0, offset - 1)
- );
- const afterTextNode = document.createTextNode(
- node.textContent.slice(offset)
- );
-
- node.parentNode.replaceChild(beforeTextNode, node);
-
- beforeTextNode.parentNode?.insertBefore(
- afterTextNode,
- beforeTextNode.nextSibling
- );
- beforeTextNode.parentNode?.insertBefore(
- mentionSelectNode,
- afterTextNode
- );
-
- const rect = mentionSelectNode.getBoundingClientRect();
- const position = {
- left: Math.floor(rect.left) - 24,
- bottom:
- Math.floor(window.innerHeight - rect.bottom) + 32,
- };
- if (!isNaN(position.left) && !isNaN(position.bottom)) {
- setAgentListPosition(position);
- }
-
- setAgentListVisible(true);
- inputNode.focus();
-
- inputNode.onblur = () => {
- let selected = agentListRef.current?.selected();
- setAgentListVisible(false);
- setTimeout(() => {
- setAgentListFilter("");
- agentListRef.current?.reset();
- });
-
- if (inputNode.getAttribute("ignore") !== "none") {
- selected = null;
- }
-
- // console.log("SELECTED", selected);
-
- // We received a selected agent configration, recover the state of the
- // contenteditable and inject an AgentMention component.
- if (selected) {
- // Construct an AgentMention component and inject it as HTML.
- const mentionNode = getAgentMentionNode(selected);
-
- // This is mainly to please TypeScript.
- if (!mentionNode || !mentionSelectNode.parentNode) {
- return;
- }
-
- // Replace mentionSelectNode with mentionNode.
- mentionSelectNode.parentNode.replaceChild(
- mentionNode,
- mentionSelectNode
- );
-
- // Prepend a space to afterTextNode (this will be the space that comes after
- // the mention).
- afterTextNode.textContent = ` ${afterTextNode.textContent}`;
-
- // If afterTextNode is the last node add an invisible character to prevent a
- // Chrome bugish behaviour ¯\_(ツ)_/¯
- if (afterTextNode.nextSibling === null) {
- afterTextNode.textContent = `${afterTextNode.textContent}\u200B`;
- }
-
- // Restore the cursor, taking into account the added space.
- range.setStart(afterTextNode, 1);
- range.setEnd(afterTextNode, 1);
- selection.removeAllRanges();
- selection.addRange(range);
- }
-
- // We didn't receive a selected agent configuration, restore the state of the
- // contenteditable and re-inject the content that was created during the
- // selection process into the contenteditable.
- if (!selected && mentionSelectNode.parentNode) {
- mentionSelectNode.parentNode.removeChild(
- mentionSelectNode
- );
-
- range.setStart(afterTextNode, 0);
- range.setEnd(afterTextNode, 0);
- selection.removeAllRanges();
- selection.addRange(range);
-
- // Insert the content of mentionSelectNode after beforeTextNode only if
- // we're not in ignore mode unless we are in ingnore mode (the user
- // backspaced into the @)
- if (
- inputNode.getAttribute("ignore") === "none" ||
- inputNode.getAttribute("ignore") === "space"
- ) {
- const newTextNode = document.createTextNode(
- (mentionSelectNode.textContent || "") +
- (inputNode.getAttribute("ignore") === "space"
- ? " "
- : "")
- );
- beforeTextNode.parentNode?.insertBefore(
- newTextNode,
- beforeTextNode.nextSibling
- );
- }
- }
- };
-
- // These are events on the small contentEditable that receives the user input
- // and drives the agent list selection.
- inputNode.onkeydown = (e) => {
- // console.log("KEYDOWN", e.key);
- if (e.key === "Escape") {
- agentListRef.current?.reset();
- inputNode.setAttribute("ignore", "escape");
- inputNode.blur();
- e.preventDefault();
- }
- if (e.key === "ArrowDown") {
- agentListRef.current?.next();
- e.preventDefault();
- }
- if (e.key === "ArrowUp") {
- agentListRef.current?.prev();
- e.preventDefault();
- }
- if (e.key === "Backspace") {
- if (inputNode.textContent === "") {
- agentListRef.current?.reset();
- inputNode.setAttribute("ignore", "backspace");
- inputNode.blur();
- e.preventDefault();
- }
- }
- if (e.key === " ") {
- if (agentListRef.current?.perfectMatch()) {
- inputNode.blur();
- e.preventDefault();
- } else {
- agentListRef.current?.reset();
- inputNode.setAttribute("ignore", "space");
- inputNode.blur();
- e.preventDefault();
- }
- }
- if (e.key === "Enter") {
- inputNode.blur();
- e.preventDefault();
- }
- };
-
- // These are the event that drive the selection of the the agent list, if we
- // have no more match we just blur to exit the selection process.
- inputNode.oninput = (e) => {
- const target = e.target as HTMLInputElement;
- // console.log("INPUT", target.textContent);
- setAgentListFilter(target.textContent || "");
- e.stopPropagation();
- setTimeout(() => {
- if (agentListRef.current?.noMatch()) {
- agentListRef.current?.reset();
- inputNode.blur();
- }
- });
- };
- }
- }
- }}
- // We aboslutely don't want any content in that div just below here. Do not add
- // anyting to it.
- >
-
- {/* This div is a spacer to avoid having the text clash with the buttons */}
-
-
-
-
-
- {
- // focus on the input text after the file selection interaction is over
- inputRef.current?.focus();
- const file = e?.target?.files?.[0];
- if (!file) return;
- if (file.size > 10_000_000) {
- sendNotification({
- type: "error",
- title: "File too large.",
- description:
- "PDF uploads are limited to 10Mb per file. Please consider uploading a smaller file.",
- });
- return;
- }
- const res = await handleFileUploadToText(file);
-
- if (res.isErr()) {
- sendNotification({
- type: "error",
- title: "Error uploading file.",
- description: res.error.message,
- });
- return;
- }
- if (res.value.content.length > 1_000_000) {
- // This error should pretty much never be triggered but it is a possible case, so here it is.
- sendNotification({
- type: "error",
- title: "File too large.",
- description:
- "The extracted text from your PDF has more than 1 million characters. This will overflow the assistant context. Please consider uploading a smaller file.",
- });
- return;
- }
- setContentFragmentFilename(res.value.title);
- setContentFragmentBody(res.value.content);
- }}
- />
- {
- fileInputRef.current?.click();
- }}
- />
- {
- // We construct the HTML for an AgentMention and inject it in the content
- // editable with an extra space after it.
- const mentionNode = getAgentMentionNode(c);
- const contentEditable =
- document.getElementById("dust-input-bar");
- if (contentEditable && mentionNode) {
- // Add mentionNode as last childe of contentEditable.
- contentEditable.appendChild(mentionNode);
- const afterTextNode = document.createTextNode(" ");
- contentEditable.appendChild(afterTextNode);
- contentEditable.focus();
- moveCursorToEnd(contentEditable);
- }
- }}
- assistants={activeAgents}
- showBuilderButtons={true}
- />
-