Repository: GooeyAI/gooey-web-widget
Pull request #20: Attach file
Author: login: anish-work
Created: 2025-05-21T11:37:21Z | Merged: 2025-05-21T12:13:20Z

### Legal Boilerplate

Look, I get it. The entity doing business as “Gooey.AI” and/or “Dara.network” was incorporated in the State of Delaware in 2020 as Dara Network Inc. and is gonna need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Dara Network Inc can use, modify, copy, and redistribute my contributions, under its choice of terms.



Diff:
diff --git a/package-lock.json b/package-lock.json
index 27878e09..7d396319 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
       "name": "gooey-chat",
       "version": "2.3.3",
       "dependencies": {
+        "@popperjs/core": "^2.11.8",
         "@sentry/react": "^9.1.0",
         "@sentry/tracing": "^7.120.3",
         "@sentry/vite-plugin": "^3.2.0",
@@ -1076,6 +1077,16 @@
         "esbuild": "^0.14.14"
       }
     },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
     "node_modules/@react-leaflet/core": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
diff --git a/package.json b/package.json
index e6ff19df..198bdbc0 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@popperjs/core": "^2.11.8",
     "@sentry/react": "^9.1.0",
     "@sentry/tracing": "^7.120.3",
     "@sentry/vite-plugin": "^3.2.0",
diff --git a/src/assets/SvgIcons/IconFile.tsx b/src/assets/SvgIcons/IconFile.tsx
new file mode 100644
index 00000000..9e1f9ae2
--- /dev/null
+++ b/src/assets/SvgIcons/IconFile.tsx
@@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconFile = (props: any) => {
+  const size = props.size || 16;
+  return (
+    <SvgIcon>
+      <svg 
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 512 512"
+        width={size}
+        height={size}
+        {...props}
+      >
+        <path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z" />
+      </svg>
+    </SvgIcon>
+  );
+};
+
+export default IconFile;
diff --git a/src/assets/SvgIcons/IconImage.tsx b/src/assets/SvgIcons/IconImage.tsx
new file mode 100644
index 00000000..102939ba
--- /dev/null
+++ b/src/assets/SvgIcons/IconImage.tsx
@@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconImage = (props: any) => {
+  const size = props.size || 16;
+  return (
+    <SvgIcon>
+      <svg 
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 512 512"
+        width={size}
+        height={size}
+      >
+        <path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/>
+      </svg>
+    </SvgIcon>
+
+  );
+};
+
+export default IconImage;
diff --git a/src/assets/SvgIcons/IconPlus.tsx b/src/assets/SvgIcons/IconPlus.tsx
new file mode 100644
index 00000000..4eae209d
--- /dev/null
+++ b/src/assets/SvgIcons/IconPlus.tsx
@@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconPlus = (props: any) => {
+  const size = props?.size || 10;
+  return (
+    <SvgIcon>
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 448 512"
+        height={size}
+        width={size}
+        {...props}
+      >
+        <path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" />
+      </svg>
+    </SvgIcon>
+  );
+};
+
+export default IconPlus;
diff --git a/src/components/shared/Buttons/Button.tsx b/src/components/shared/Buttons/Button.tsx
index ce8138c8..eb7b57c3 100644
--- a/src/components/shared/Buttons/Button.tsx
+++ b/src/components/shared/Buttons/Button.tsx
@@ -12,8 +12,10 @@ export interface ButtonProps
   className?: string;
   variant?: "filled" | "contained" | "outlined" | "text" | "text-alt";
   RightIconComponent?: React.FC<any>;
+  LeftIconComponent?: React.FC<any>;
   showIconOnHover?: boolean;
   hideOverflow?: boolean;
+  isPressed?: boolean;
 }
 
 const Button = ({
@@ -22,8 +24,10 @@ const Button = ({
   className = "",
   onClick,
   RightIconComponent,
+  LeftIconComponent,
   showIconOnHover,
   hideOverflow,
+  isPressed,
   ...rest
 }: ButtonProps) => {
   const variantClasses = `button-${variant?.toLowerCase()}`;
@@ -32,7 +36,7 @@ const Button = ({
     <button
       {...rest}
       onMouseDown={onClick}
-      className={variantClasses + " " + className}
+      className={clsx(variantClasses, className, isPressed && "depressed")}
     >
       <div
         className={clsx(
@@ -40,18 +44,36 @@ const Button = ({
           hideOverflow && "btn-hide-overflow",
         )}
       >
-        {rest.children}
-        {RightIconComponent && (
+        {LeftIconComponent && (
           <div
             className={clsx(
-              "btn-icon right-icon",
-              "flex items-center justify-center",
+              "btn-icon-left",
+              "d-flex items-center",
               showIconOnHover && "icon-hover",
             )}
+            style={{
+              width: "fit-content",
+              gap: "4px",
+            }}
           >
-            <RightIconComponent />
+            <LeftIconComponent />
+            {rest.children}
           </div>
         )}
+        {!RightIconComponent && !LeftIconComponent && (rest.children)}
+        {RightIconComponent && (
+          <div
+            className={clsx("d-flex items-center",
+              showIconOnHover && "icon-hover",
+            )}
+          >
+            {rest.children}
+            <span className="btn-icon-right">
+              <RightIconComponent />
+            </span>
+          </div>
+        )}
+
         {hideOverflow && <div className="button-right-blur" />}
       </div>
     </button>
diff --git a/src/components/shared/Buttons/IconButton.tsx b/src/components/shared/Buttons/IconButton.tsx
index ee714065..a4333c2a 100644
--- a/src/components/shared/Buttons/IconButton.tsx
+++ b/src/components/shared/Buttons/IconButton.tsx
@@ -5,9 +5,10 @@ const IconButton = ({
   className,
   variant = "text",
   onClick,
+  isPressed,
   ...rest
 }: ButtonProps) => {
-  const btnClasses = clsx(`button-${variant?.toLowerCase()}`, className);
+  const btnClasses = clsx(`button-${variant?.toLowerCase()}`, className, isPressed && "depressed");
   return (
     <button {...rest} className={btnClasses} onClick={onClick}>
       {rest.children}
diff --git a/src/components/shared/Buttons/buttons.scss b/src/components/shared/Buttons/buttons.scss
index 8b441b2f..682df22e 100644
--- a/src/components/shared/Buttons/buttons.scss
+++ b/src/components/shared/Buttons/buttons.scss
@@ -22,12 +22,15 @@ button:disabled {
   cursor: unset;
 }
 
-button .btn-icon {
+button .btn-icon-left {
+  display: inline;
+}
+
+button .btn-icon-right {
   position: absolute;
   top: 50%;
   transform: translateY(-50%);
   right: 0;
-  z-index: 2;
 }
 
 button .icon-hover {
@@ -44,6 +47,10 @@ button:hover .icon-hover {
   opacity: 1;
 }
 
+button.depressed {
+  background-color: $light;
+}
+
 // Variant - FILLED
 .button-filled {
   background-color: #eee;
diff --git a/src/components/shared/Loaders.tsx b/src/components/shared/Loaders.tsx
index 219e5145..b35853db 100644
--- a/src/components/shared/Loaders.tsx
+++ b/src/components/shared/Loaders.tsx
@@ -1,32 +1,6 @@
-export const SkeletonBox = () => {
-  return (
-    <div
-      className="loading-skeleton bg-grey"
-      style={{ width: "100px", height: "100px" }}
-    ></div>
-  );
-};
-
 export const CircularLoader = (props: any) => {
   const size = props.size || 16;
   return (
-    <div className="circular-loader">
-      <svg
-        className="circular"
-        viewBox="25 25 50 50"
-        height={size}
-        width={size}
-      >
-        <circle
-          className="path"
-          cx="50"
-          cy="50"
-          r="20"
-          fill="none"
-          stroke-width="2"
-          stroke-miterlimit="10"
-        ></circle>
-      </svg>
-    </div>
+    <div className="circular-loader" style={{ width: size, height: size }} />
   );
 };
diff --git a/src/components/shared/Popper/Popper.tsx b/src/components/shared/Popper/Popper.tsx
index 7246d89a..35874c6a 100644
--- a/src/components/shared/Popper/Popper.tsx
+++ b/src/components/shared/Popper/Popper.tsx
@@ -1,5 +1,6 @@
-import { useState } from "react";
+import { useRef, useEffect } from "react";
 import { createPortal } from "react-dom";
+import { createPopper, Placement } from "@popperjs/core";
 import { addInlineStyle } from "src/addStyles";
 import style from "./popper.scss?inline";
 
@@ -12,129 +13,95 @@ type PopperDirection = {
 
 interface PopperProps extends React.HTMLAttributes<HTMLDivElement> {
   children: React.ReactNode;
-  ModalContent?: any;
+  ModalContent?: () => React.ReactNode;
   direction?: PopperDirection;
   showModal: boolean;
   ModalProps?: Record<string, any>;
 }
 
-const getModalCoordinates = (
-  containerRef: HTMLElement,
-  direction: PopperDirection
-) => {
-  const containerCoordinates = containerRef.getBoundingClientRect();
-  let translateX = "0px";
-  let translateY = "0px";
-  const x = direction.x;
-  const y = direction.y;
-  const modalCoordinates = {
-    top: 0,
-    left: 0,
-    transform: "none",
-  };
-  switch (x) {
-    case "left":
-      modalCoordinates.left =
-        containerCoordinates.left - containerCoordinates.width;
-      translateX = "calc(-50% - 12px)";
-      break;
-    case "right":
-      modalCoordinates.left = containerCoordinates.right;
-      translateX = "12px";
-      break;
-    case "center":
-      modalCoordinates.left =
-        containerCoordinates.left + containerCoordinates.width / 2;
-      modalCoordinates.transform = "translate(-50%, 12px)";
-      translateY = "12px";
-      translateX = "-50%";
-      break;
-  }
-
-  switch (y) {
-    case "top":
-      modalCoordinates.top = containerCoordinates.top - 12;
-      if (x === "center") modalCoordinates.transform = "translate(-50%, -100%)";
-      else modalCoordinates.transform = "translate(0, -100%)";
-      translateY = "0";
-      if (translateX === "0") translateX = "-100%";
-      break;
-    case "bottom":
-      modalCoordinates.top = containerCoordinates.bottom;
-      break;
-    case "center":
-      modalCoordinates.top =
-        containerCoordinates.top + containerCoordinates.height / 2;
-      translateY = "-50%";
-      if (translateX === "0") translateX = "12px";
-      break;
-  }
-  modalCoordinates.transform = `translate(${translateX}, ${translateY})`;
-  return modalCoordinates;
+// Map your direction prop to Popper.js placement
+const getPopperPlacement = (direction: PopperDirection): Placement => {
+  const { x, y } = direction;
+  if (y === "top") return x === "left" ? "top-start" : x === "right" ? "top-end" : "top";
+  if (y === "bottom") return x === "left" ? "bottom-start" : x === "right" ? "bottom-end" : "bottom";
+  if (x === "left") return "left";
+  if (x === "right") return "right";
+  if (x === "center") return "top-start";
+  return "bottom";
 };
 
 const Modal = ({
-  containerRef,
+  referenceElement,
   direction,
-  style,
-  className = "",
   ModalContent,
-  ...restProps
+  style: styleProp,
+  className = "",
+  showModal,
+  ...rest
 }: {
-  ModalContent: any;
-  containerRef: HTMLElement | null;
+  ModalContent: () => React.ReactNode;
+  referenceElement: HTMLElement | null;
   direction: PopperDirection;
   style?: React.CSSProperties;
   className?: string;
+  showModal: boolean;
 }) => {
-  if (!containerRef) return null;
-  const modalCoordinates = getModalCoordinates(containerRef, direction);
-  if (!ModalContent) return null;
+  const popperRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (referenceElement && popperRef.current && showModal) {
+      const instance = createPopper(referenceElement, popperRef.current, {
+        placement: getPopperPlacement(direction),
+        modifiers: [
+          { name: "preventOverflow", options: { padding: 8 } },
+          { name: "flip", options: { fallbackPlacements: ["top", "bottom", "right", "left"] } },
+        ],
+      });
+      return () => {
+        instance.destroy();
+      };
+    }
+  }, [referenceElement, direction, showModal]);
+
+  if (!referenceElement || !ModalContent || !showModal) return null;
   return (
     <div
-      className={(className += " gooey-modal")}
-      style={{
-        ...style,
-        ...modalCoordinates,
-      }}
-      {...restProps}
+      ref={popperRef}
+      className={`gooey-modal ${className}`.trim()}
+      style={styleProp}
+      {...rest}
     >
       {ModalContent()}
     </div>
   );
 };
 
-const GooeyPopper = (props: PopperProps) => {
-  const {
-    ModalContent = () => null,
-    children,
-    direction = {
-      x: "center",
-      y: "bottom",
-    },
-    showModal,
-    ModalProps,
-    ...restProps
-  } = props;
-
-  const [refContainer, setRefContainer] = useState<HTMLElement | null>(null);
+const GooeyPopper = ({
+  ModalContent = () => null,
+  children,
+  direction = { x: "center", y: "bottom" },
+  showModal,
+  ModalProps,
+  ...rest
+}: PopperProps) => {
+  const refContainer = useRef<HTMLDivElement>(null);
   return (
     <div
       className="gooey-clipping-container"
-      ref={setRefContainer}
-      {...restProps}
+      ref={refContainer}
+      {...rest}
     >
       {children}
       {showModal &&
         createPortal(
           <Modal
-            containerRef={refContainer}
+            referenceElement={refContainer.current}
             direction={direction}
-            ModalContent={ModalContent as React.FC<any>}
+            ModalContent={ModalContent}
+            showModal={showModal}
             {...ModalProps}
           />,
-          gooeyShadowRoot?.querySelector(".gooey-embed-container") ||
-            (document.body as Element)
+          gooeyShadowRoot?.querySelector(".gooey-embed-container") || document.body
         )}
     </div>
   );
diff --git a/src/components/shared/Popper/popper.scss b/src/components/shared/Popper/popper.scss
index 695c4c89..4fe1dbb7 100644
--- a/src/components/shared/Popper/popper.scss
+++ b/src/components/shared/Popper/popper.scss
@@ -4,11 +4,10 @@
 }
 
 .gooey-modal {
-  transition: all 0.3s;
+  transition: opacity 0.3s;
   position: fixed;
   width: max-content;
   background-color: white;
-  padding: 10px;
   border-radius: 8px;
   z-index: 99999;
   font-weight: lighter;
diff --git a/src/components/shared/Tooltip.tsx b/src/components/shared/Tooltip.tsx
index 26320dff..b55da791 100644
--- a/src/components/shared/Tooltip.tsx
+++ b/src/components/shared/Tooltip.tsx
@@ -89,7 +89,7 @@ const GooeyTooltip = ({
   return (
     <GooeyPopper
       ModalContent={() => (
-        <>
+        <div className="gp-8">
           <div
             style={{
               position: "absolute",
@@ -99,7 +99,7 @@ const GooeyTooltip = ({
             }}
           />
           <p className="font_14_500">{text}</p>
-        </>
+        </div>
       )}
       showModal={showModal}
       direction={{
diff --git a/src/contexts/MessagesContext.tsx b/src/contexts/MessagesContext.tsx
index 0ca8501b..5fcbbe99 100644
--- a/src/contexts/MessagesContext.tsx
+++ b/src/contexts/MessagesContext.tsx
@@ -69,6 +69,7 @@ const MessagesContextProvider = (props: any) => {
     const conversationId = lastResponse?.conversation_id;
     setIsSendingMessage(true);
     const newQuery = createNewQuery(payload);
+    if (payload?.attached_files?.length) delete payload.attached_files;
     sendPayload({
       ...payload,
       conversation_id: conversationId,
diff --git a/src/contexts/SystemContext.tsx b/src/contexts/SystemContext.tsx
index 3379f5dc..b7c80764 100644
--- a/src/contexts/SystemContext.tsx
+++ b/src/contexts/SystemContext.tsx
@@ -158,6 +158,7 @@ const SystemContextProvider = ({
       },
       toggleSecondaryDrawer: (data = null) => {
         setLayoutState((prev: any) => {
+          if(!data && !prev?.isSecondaryDrawerOpen) return prev;
           const triggerSidebar =
             data && prev.isSidebarOpen && !prev.isSecondaryDrawerOpen;
           if (triggerSidebar) toggleSidebarStyles(prev.isSidebarOpen);
diff --git a/src/css/_animations.scss b/src/css/_animations.scss
index d9f74946..0b9545b5 100644
--- a/src/css/_animations.scss
+++ b/src/css/_animations.scss
@@ -85,6 +85,23 @@
   }
 }
 
+.circular-loader {
+  width: 10px;
+  padding: 4px;
+  aspect-ratio: 1;
+  border-radius: 50%;
+  background: $light;
+  --_m: 
+    conic-gradient(#0000 10%,#000),
+    linear-gradient(#000 0 0) content-box;
+  -webkit-mask: var(--_m);
+          mask: var(--_m);
+  -webkit-mask-composite: source-out;
+          mask-composite: subtract;
+  animation: l3 1s infinite linear;
+}
+@keyframes l3 {to{transform: rotate(1turn)}}
+
 @keyframes rotate {
   0% {
     transform: rotate(0deg);
diff --git a/src/css/_extra.scss b/src/css/_extra.scss
index fd36303e..ac594ead 100644
--- a/src/css/_extra.scss
+++ b/src/css/_extra.scss
@@ -206,6 +206,7 @@
   -ms-overflow-style: none;
   padding-right: 20px; /* Prevent content from being cut off */
   white-space: nowrap;
+  position: relative;
 }
 
 @media (max-width: $tablet) {
diff --git a/src/widgets/copilot/components/ChatInput/FilePreview.tsx b/src/widgets/copilot/components/ChatInput/FilePreview.tsx
index 2da35047..76059c4e 100644
--- a/src/widgets/copilot/components/ChatInput/FilePreview.tsx
+++ b/src/widgets/copilot/components/ChatInput/FilePreview.tsx
@@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "50%",
+            left: "50%",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <span style={{ zIndex: 2 }}>
+            <CircularLoader size={32} />
+          </span>
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            right: "-16px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-4 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile();
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <div
+        className={clsx(
+          isUploading && "uploading-box",
+          "overflow-hidden file-preview-box",
+        )}
+      >
+        <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
+      </div>
+    </div>
+  );
+};
+
+export const FilePreviewItem = ({
+  file,
+  removeFile = () => {},
+  onClick,
+  isUploading,
+  isRemovable,
+}: {
+  file: any;
+  removeFile?: (e: SyntheticEvent) => void;
+  onClick: (e: SyntheticEvent) => void;
+  isUploading: boolean;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx(
+        "b-1 gp-10 br-default pos-relative d-flex align-center cr-pointer",
+      )}
+      style={{ width: "200px" }}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "0",
+            left: "0",
+            width: "100%",
+            height: "100%",
+            backgroundColor: "rgba(0, 0, 0, 0.5)",
+          }}
+          className="d-flex br-default align-center justify-center"
+        >
+          <CircularLoader size={32} />
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            left: "4px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 4,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-2 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile(e);
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <IconFile size={24} style={{ minWidth: "fit-content" }} />
+      {/* remove extension */}
+      <div
+        style={{
+          width: "90%",
+          overflow: "hidden",
+          textOverflow: "ellipsis",
+          whiteSpace: "nowrap",
+        }}
+      >
+        <p
+          className="font_14_500"
+          style={{ width: "90%", textOverflow: "ellipsis" }}
+        >
+          {truncateMiddle(file.name.split(".")[0], 20)}
+        </p>
+        <p className="font_12_400 text-muted">
+          {file.name.split(".")[1].toUpperCase()}
+        </p>
+      </div>
+    </div>
+  );
+};
 export default FilePreview;
diff --git a/src/widgets/copilot/components/ChatInput/chatInput.scss b/src/widgets/copilot/components/ChatInput/chatInput.scss
index 34df7262..131f592c 100644
--- a/src/widgets/copilot/components/ChatInput/chatInput.scss
+++ b/src/widgets/copilot/components/ChatInput/chatInput.scss
@@ -32,11 +32,21 @@
 }
 
 .file-preview-box img {
-  height: 80px;
-  max-width: 100px;
+  height: 60px;
+  width: 100px;
   object-fit: cover;
 }
 
 .uploading-box {
   filter: brightness(0.2);
 }
+
+.gooey-menu-item {
+  padding: 4px;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.gooey-menu-item:hover {
+  background-color: $light;
+}
diff --git a/src/widgets/copilot/components/ChatInput/index.tsx b/src/widgets/copilot/components/ChatInput/index.tsx
index eb1c8210..fb22194a 100644
--- a/src/widgets/copilot/components/ChatInput/index.tsx
+++ b/src/widgets/copilot/components/ChatInput/index.tsx
@@ -10,14 +10,31 @@ import InlineAudioRecorder from "./InlineAudioRecorder";
 
 import { addInlineStyle } from "src/addStyles";
 import style from "./chatInput.scss?inline";
-import IconPaperClip from "src/assets/SvgIcons/IconPaperClip";
 import FilePreview from "./FilePreview";
 import { uploadFileToGooey } from "src/api/file-upload";
+import IconPlus from "src/assets/SvgIcons/IconPlus";
+import GooeyPopper from "src/components/shared/Popper/Popper";
+import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+import IconImage from "src/assets/SvgIcons/IconImage";
+import { v4 as uuidv4 } from "uuid";
 addInlineStyle(style);
 
 export const CHAT_INPUT_ID = "gooeyChat-input";
 const INPUT_HEIGHT = 44;
-const acceptedTypes = "image/*";
+const acceptedFileTypes = "application/*, text/*";
+const acceptedImageTypes = "image/*, video/*";
+
+// Define a type for file state
+interface UploadedFile {
+  id: string;
+  name: string;
+  type: string;
+  data: File;
+  gooeyUrl: string;
+  isUploading: boolean;
+  removeFile: () => void;
+}
 
 const makeFileBuffer = (file: File) => {
   return new Promise((resolve, reject) => {
@@ -38,8 +55,10 @@ const ChatInput = () => {
     useMessagesContext();
   const [value, setValue] = useState("");
   const [isRecording, setIsRecording] = useState(false);
-  const [files, setFiles] = useState<Array<any> | null>(null);
+  const [files, setFiles] = useState<UploadedFile[] | null>(null);
   const [preAttachedFileUsed, setPreAttachedFileUsed] = useState(false);
+  const [isMenuOpen, setIsMenuOpen] = useState(false);
+  const menuButtonRef = useRef<HTMLDivElement | null>(null);
 
   const inputRef = useRef<null | HTMLElement>(null);
 
@@ -52,26 +71,44 @@ const ChatInput = () => {
       if (typeof image === "string") return; // @TODO: support for CDN URLs
       // check if image is { name: string; mime: string; bytes: number[]; };
       if (typeof image === "object" && image.bytes) {
-        const fileObj = new File([new Uint8Array(image.bytes)], image.name || "gooey-image.png", { type: image.mime || "image/png" });
+        const fileObj = new File(
+          [new Uint8Array(image.bytes)],
+          image.name || "gooey-image.png",
+          { type: image.mime || "image/png" },
+        );
         setFiles(processFiles([fileObj]));
         setPreAttachedFileUsed(true);
       }
     });
-
   }, [config?.payload?.input_images, preAttachedFileUsed]);
 
+  // Dismiss menu on outside click
+  useEffect(() => {
+    if (!isMenuOpen) return;
+    const handleClick = (e: MouseEvent) => {
+      if (
+        menuButtonRef.current &&
+        !menuButtonRef.current.contains(e.target as Node)
+      ) {
+        setIsMenuOpen(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClick);
+    return () => document.removeEventListener("mousedown", handleClick);
+  }, [isMenuOpen]);
+
   const resetHeight = () => {
     const ele: HTMLElement | null = inputRef.current;
     ele!.style!.height = INPUT_HEIGHT + "px";
   };
 
-  const handleInputChange = (e: any) => {
+  const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
     const { value } = e.target;
     setValue(value);
     if (!value) resetHeight();
   };
 
-  const handlePressEnter = (e: any) => {
+  const handlePressEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
     if (e.keyCode === 13 && !e.shiftKey) {
       if (isSending || isReceiving) return;
       e.preventDefault();
@@ -87,7 +124,7 @@ const ChatInput = () => {
     if (ele!.scrollHeight > INPUT_HEIGHT)
       ele?.setAttribute(
         "style",
-        "height:" + ele.scrollHeight + "px !important"
+        "height:" + ele.scrollHeight + "px !important",
       );
   };
 
@@ -97,7 +134,23 @@ const ChatInput = () => {
       input_prompt: value.trim(),
     };
     if (files?.length) {
-      payload.input_images = files.map((file) => file.gooeyUrl);
+      const documents = files.filter(
+        (file) => file.type === "application" || file.type === "text",
+      );
+      const images = files.filter((file) => file.type === "image");
+      const videos = files.filter((file) => file.type === "video");
+
+      // attach to payload for server to process
+      if (documents.length)
+        payload.input_documents = documents.map((file) => file.gooeyUrl);
+      if (images.length)
+        payload.input_images = images.map((file) => file.gooeyUrl);
+      if (videos.length)
+        payload.input_documents = [
+          ...(payload.input_documents || []),
+          ...videos.map((file) => file.gooeyUrl),
+        ];
+      payload.attached_files = files ? [...files] : [];
       setFiles([]);
     }
     initializeQuery(payload);
@@ -119,30 +172,41 @@ const ChatInput = () => {
   };
 
   const processFiles = (files: Array<any>) => {
-    return files.map((file: any, index: number) => {
+    return files.map((file: any) => {
+      const id = uuidv4();
       makeFileBuffer(file).then((blob) => {
         const toUpload = new File([blob as Blob], file.name);
-        uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
-          setFiles((prev: any) => {
-            if (!prev[index]) return prev; // if photo removed before upload completed
-            prev[index].isUploading = false;
-            prev[index].gooeyUrl = url;
-            return [...prev];
+        try {
+          if (!config || !config.apiUrl) return;
+          uploadFileToGooey(config.apiUrl, toUpload).then((url) => {
+            setFiles((prev: any) => {
+              const idx = prev.findIndex((f: any) => f.id === id);
+              if (idx === -1) return prev; // if photo removed before upload completed
+              const updated = [...prev];
+              updated[idx] = {
+                ...updated[idx],
+                isUploading: false,
+                gooeyUrl: url,
+              };
+              return updated;
+            });
           });
-        });
+        } catch (err) {
+          console.error(err);
+          setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
+          // TODO: show error toast
+        }
       });
 
       return {
+        id,
         name: file.name,
         type: file.type.split("/")[0],
         data: file,
         gooeyUrl: "",
         isUploading: true,
-        removeFile: () => {
-          setFiles((prev: any) => {
-            prev.splice(index, 1);
-            return [...prev];
-          });
+        removeFile: function () {
+          setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
         },
       };
     });
@@ -151,14 +215,28 @@ const ChatInput = () => {
   const onFileAdded = (e: any) => {
     const files = Array.from(e.target.files);
     if (!files || !files.length) return;
-    setFiles(processFiles(files));
+    setFiles((prev: any) =>
+      prev ? [...prev, ...processFiles(files)] : processFiles(files),
+    );
   };
 
-  const handleFileUpload = () => {
+  const handleFileMenuClick = () => {
+    setIsMenuOpen(false);
     const input = document.createElement("input");
     input.type = "file";
-    input.accept = acceptedTypes;
+    input.accept = acceptedFileTypes;
     input.onchange = onFileAdded;
+    input.multiple = true;
+    input.click();
+  };
+
+  const handlePhotoMenuClick = () => {
+    setIsMenuOpen(false);
+    const input = document.createElement("input");
+    input.type = "file";
+    input.accept = acceptedImageTypes;
+    input.onchange = onFileAdded;
+    input.multiple = true;
     input.click();
   };
 
@@ -169,19 +247,19 @@ const ChatInput = () => {
     files?.some((file) => file.isUploading);
   const isLeftButtons = useMemo(
     () => config?.enablePhotoUpload,
-    [config?.enablePhotoUpload]
+    [config?.enablePhotoUpload],
   );
   return (
     <React.Fragment>
       {files && files.length > 0 && (
         <div className="gp-12 b-1 br-large gmb-12 gm-12">
-          <FilePreview files={files} />
+          <FilePreview files={files} isRemovable={true} />
         </div>
       )}
       <div
         className={clsx(
           "gooeyChat-chat-input gpr-8 gpl-8",
-          !config.branding.showPoweredByGooey && "gpb-8"
+          !config.branding.showPoweredByGooey && "gpb-8",
         )}
       >
         {isRecording ? (
@@ -200,7 +278,7 @@ const ChatInput = () => {
               onKeyDown={handlePressEnter}
               className={clsx(
                 "br-large b-1 font_16_500 bg-white gpt-10 gpb-10 gpr-40 flex-1 gm-0",
-                isLeftButtons ? "gpl-32" : "gpl-12"
+                isLeftButtons ? "gpl-32" : "gpl-12",
               )}
               placeholder={`Message ${config.branding.name || ""}`}
             ></textarea>
@@ -208,13 +286,42 @@ const ChatInput = () => {
             {/* Left icons */}
             {isLeftButtons && (
               <div className="input-left-buttons">
-                <IconButton
-                  onClick={handleFileUpload}
-                  variant="text-alt"
-                  className="gp-4"
+                <GooeyPopper
+                  showModal={isMenuOpen}
+                  direction={{ x: "left", y: "top" }}
+                  ModalContent={() => (
+                    <div className="gp-8">
+                      <Button
+                        className="w-100 text-left"
+                        style={{ minWidth: "100px" }}
+                        variant="text-alt"
+                        onClick={handleFileMenuClick}
+                        LeftIconComponent={() => <IconFile size={16} />}
+                      >
+                        <p className="font_14_500">File</p>
+                      </Button>
+                      <Button
+                        className="w-100 text-left"
+                        variant="text-alt"
+                        onClick={handlePhotoMenuClick}
+                        LeftIconComponent={() => <IconImage size={16} />}
+                      >
+                        <p className="font_14_500">Image or Video </p>
+                      </Button>
+                    </div>
+                  )}
                 >
-                  <IconPaperClip size={18} />
-                </IconButton>
+                  <div ref={menuButtonRef} style={{ display: "inline-block" }}>
+                    <IconButton
+                      onClick={() => setIsMenuOpen((v) => !v)}
+                      variant="text-alt"
+                      isPressed={isMenuOpen}
+                      className={clsx("gp-4", isMenuOpen && "depressed")}
+                    >
+                      <IconPlus size={18} />
+                    </IconButton>
+                  </div>
+                </GooeyPopper>
               </div>
             )}
 
@@ -233,15 +340,15 @@ const ChatInput = () => {
                 !config?.enableAudioMessage ||
                 showStop ||
                 !!files?.length) && (
-                  <IconButton
-                    disabled={disableSend}
-                    variant="text-alt"
-                    className="gp-4"
-                    onClick={showStop ? handleCancelSend : handleSendMessage}
-                  >
-                    {showStop ? <CircleStop size={24} /> : <CircleUP size={24} />}
-                  </IconButton>
-                )}
+                <IconButton
+                  disabled={disableSend}
+                  variant="text-alt"
+                  className="gp-4"
+                  onClick={showStop ? handleCancelSend : handleSendMessage}
+                >
+                  {showStop ? <CircleStop size={24} /> : <CircleUP size={24} />}
+                </IconButton>
+              )}
             </div>
           </div>
         )}
diff --git a/src/widgets/copilot/components/Messages/OutgoingMsg.tsx b/src/widgets/copilot/components/Messages/OutgoingMsg.tsx
index 1ccce20a..8660ef75 100644
--- a/src/widgets/copilot/components/Messages/OutgoingMsg.tsx
+++ b/src/widgets/copilot/components/Messages/OutgoingMsg.tsx
@@ -1,7 +1,7 @@
 import { addInlineStyle } from "src/addStyles";
 import style from "./outgoing.scss?inline";
 import { memo } from "react";
-import clsx from "clsx";
+import FilePreview from "../ChatInput/FilePreview";
 addInlineStyle(style);
 
 interface ButtonPressed {
@@ -14,20 +14,22 @@ interface OutgoingMsgProps {
   input_prompt?: string;
   input_audio?: Blob;
   input_images?: string[];
+  input_documents?: string[];
   button_pressed?: ButtonPressed;
   input_location?: {
     latitude?: number;
     longitude?: number;
   };
+  attached_files?: any[];
 }
 
 const OutgoingMsg = memo(
   ({
     input_prompt = "",
     input_audio = undefined,
-    input_images = [],
     button_pressed = undefined,
     input_location: { latitude, longitude } = {},
+    attached_files = [],
   }: OutgoingMsgProps) => {
     let mapUrl;
     if (latitude && longitude) {
@@ -38,7 +40,7 @@ const OutgoingMsg = memo(
 
     return (
       <div className="gooey-outgoingMsg gmb-12 gpl-16">
-        {input_images.length > 0 &&
+        {/* {input_images.length > 0 &&
           input_images.map((url: string) => (
             <a href={url} target="_blank">
               <img
@@ -50,7 +52,7 @@ const OutgoingMsg = memo(
                 )}
               />
             </a>
-          ))}
+          ))} */}
         {input_audio && (
           <div className="gmt-16">
             <audio
@@ -59,6 +61,11 @@ const OutgoingMsg = memo(
             ></audio>
           </div>
         )}
+        {attached_files && attached_files.length > 0 && (
+          <div className="gmt-16">
+            <FilePreview files={attached_files} />
+          </div>
+        )}
         {input_prompt && (
           <p className="font_20_400 anim-typing gooey-outgoing-text">
             {input_prompt}
diff --git a/src/widgets/copilot/components/Messages/Sources.tsx b/src/widgets/copilot/components/Messages/Sources.tsx
index 68ada087..fde1cde7 100644
--- a/src/widgets/copilot/components/Messages/Sources.tsx
+++ b/src/widgets/copilot/components/Messages/Sources.tsx
@@ -6,6 +6,7 @@ import {
   fetchUrlMeta,
   findSourceIcon,
   getEmbedUrl,
+  renderImageInIframe,
   truncateMiddle,
 } from "./helpers";
 import { useSystemContext } from "src/contexts/hooks";
@@ -27,8 +28,8 @@ export const FullSourcePreview = (props: any) => {
     }, 0);
   }, [data.url]);
 
-  const embedUrl = getEmbedUrl(data.url);
-  if (!data || !data?.url) return null;
+  if (!data || (!data?.url && !data?.isImage)) return null;
+  const embedUrl = data?.isImage ? renderImageInIframe(data.url) : getEmbedUrl(data.url);
   const ExtensionIcon: any = findSourceIcon(
     metaData?.content_type,
     metaData?.redirect_urls[0] || data?.url,
diff --git a/src/widgets/copilot/components/Messages/helpers.tsx b/src/widgets/copilot/components/Messages/helpers.tsx
index 36f7779a..27d1350f 100644
--- a/src/widgets/copilot/components/Messages/helpers.tsx
+++ b/src/widgets/copilot/components/Messages/helpers.tsx
@@ -132,7 +132,14 @@ export const fetchUrlMeta = async (url: string) => {
 
 // Text Rendering Logic
 const getOutputText = (data: any) => {
-  const { type = "", status = "", text, detail, output_text = {}, raw_output_text = {} } = data;
+  const {
+    type = "",
+    status = "",
+    text,
+    detail,
+    output_text = {},
+    raw_output_text = {},
+  } = data;
   let out = "";
   if (type === STREAM_MESSAGE_TYPES.MESSAGE_PART) {
     if (text) {
@@ -145,7 +152,7 @@ const getOutputText = (data: any) => {
   if (type === STREAM_MESSAGE_TYPES.FINAL_RESPONSE && status === "completed") {
     out = output_text[0] || raw_output_text[0];
   }
-  
+
   // replace 🎧 I heard from out
   out = out.replace("🎧 I heard", "🎙️");
   return out;
@@ -331,6 +338,62 @@ export function truncateMiddle(str: string, charLimit: number) {
   return str.slice(0, frontChars) + ellipsis + str.slice(-backChars);
 }
 
+export function isGoogleDocsEmbeddable(input: string): boolean {
+  const supportedMimeTypes = new Set([
+    "application/pdf",
+    "application/msword",
+    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+    "application/vnd.ms-powerpoint",
+    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+    "application/vnd.ms-excel",
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    "text/plain",
+    "application/rtf",
+    "text/csv",
+  ]);
+
+  const supportedExtensions = new Set([
+    ".pdf",
+    ".doc",
+    ".docx",
+    ".ppt",
+    ".pptx",
+    ".xls",
+    ".xlsx",
+    ".txt",
+    ".rtf",
+  ]);
+
+  // MIME type
+  if (input.includes("/") && !input.startsWith("http")) {
+    return supportedMimeTypes.has(input);
+  }
+
+  try {
+    const url = new URL(input);
+    let pathname = url.pathname.toLowerCase();
+
+    // Remove trailing slash if present
+    if (pathname.endsWith("/")) {
+      pathname = pathname.slice(0, -1);
+    }
+
+    // Extract filename from path
+    const filename = pathname.split("/").pop() || "";
+
+    // Check if the filename ends with any of the supported extensions
+    let match = false;
+    for (const ext of supportedExtensions) {
+      if (filename.endsWith(ext)) {
+        match = true;
+        break;
+      }
+    }
+    return match;
+  } catch {
+    return false;
+  }
+}
 export const getEmbedUrl = (url: string) => {
   try {
     // Check if it's a YouTube URL
@@ -343,10 +406,46 @@ export const getEmbedUrl = (url: string) => {
       return `https://www.youtube.com/embed/${match[1]}`;
     }
 
-    // Return original URL if not YouTube
+    if (isGoogleDocsEmbeddable(url)) {
+      return `https://docs.google.com/gview?url=${encodeURIComponent(url)}&embedded=true`;
+    }
+
+    // Return original
     return url;
   } catch (error) {
     console.error("Error processing URL:", error);
     return url;
   }
 };
+export function renderImageInIframe(imgBlobUrl: string) {
+  const html = `
+    <html>
+      <head>
+        <style>
+          html, body {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+            width: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: white;
+          }
+          img {
+            max-width: 100%;
+            max-height: 100%;
+            object-fit: contain;
+          }
+        </style>
+      </head>
+      <body>
+        <img src="${imgBlobUrl}" />
+      </body>
+    </html>
+  `;
+
+  const blob = new Blob([html], { type: 'text/html' });
+  const blobUrl = URL.createObjectURL(blob);
+  return blobUrl;
+}
diff --git a/src/widgets/copilot/components/Messages/index.tsx b/src/widgets/copilot/components/Messages/index.tsx
index 8aa77e05..d66cab17 100644
--- a/src/widgets/copilot/components/Messages/index.tsx
+++ b/src/widgets/copilot/components/Messages/index.tsx
@@ -26,6 +26,7 @@ const Responses = (props: any) => {
             input_images={responseData.input_images}
             button_pressed={responseData.button_pressed}
             input_location={responseData.input_location}
+            attached_files={responseData.attached_files}
           />;
         } else {
           return (
@@ -55,7 +56,7 @@ const Messages = () => {
 
   useEffect(() => {
     // avoid autoplay on mount
-    avoidAutoplay();
+    if (avoidAutoplay) avoidAutoplay();
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T11:39:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/widgets/copilot/components/Messages/OutgoingMsg.tsx (1)</summary><blockquote>

`43-55`: _🛠️ Refactor suggestion_

**Remove commented code instead of keeping it.**

Commented-out code should be removed rather than committed, as it clutters the codebase and creates maintenance issues.

```diff
-  {/* {input_images.length > 0 &&
-    input_images.map((url: string) => (
-      <a href={url} target="_blank">
-        <img
-          src={url}
-          alt={url}
-          className={clsx(
-            "outgoingMsg-image b-1 br-large",
-            input_prompt && "gmb-4",
-          )}
-        />
-      </a>
-    ))} */}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (14)</summary><blockquote>

<details>
<summary>src/components/shared/Tooltip.tsx (1)</summary><blockquote>

`92-102`: **Ensure proper ARIA and utility‐class usage for the new wrapper**  
Wrapping the tooltip content in `<div className="gp-8">` aligns with styling updates, but to maintain accessibility and robustness:  
- Add `role="tooltip"` to the container for screen readers.  
- Tie `aria-hidden` to the visibility state (e.g., `aria-hidden={!showModal}`) if the popper doesn’t already manage it.  
- Verify that `gp-8` exists in your utility CSS and applies the intended spacing.  


Example diff to get started:
```diff
-        <div className="gp-8">
+        <div className="gp-8" role="tooltip" aria-hidden={!showModal}>
```

</blockquote></details>
<details>
<summary>src/contexts/MessagesContext.tsx (1)</summary><blockquote>

`72-72`: **Consider using a more performant approach instead of the `delete` operator**

While the logic for removing attached files from the payload is correct, using the `delete` operator can impact JavaScript engine optimization. Consider using destructuring or creating a filtered copy of the payload instead.


```diff
-    if (payload?.attached_files?.length) delete payload.attached_files;
+    if (payload?.attached_files?.length) {
+      const { attached_files, ...payloadWithoutFiles } = payload;
+      payload = payloadWithoutFiles;
+    }
```

Alternatively:

```diff
-    if (payload?.attached_files?.length) delete payload.attached_files;
+    if (payload?.attached_files?.length) {
+      payload = { ...payload };
+      payload.attached_files = undefined;
+    }
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 72-72: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.


(lint/performance/noDelete)

</details>

</details>

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/Messages/Sources.tsx (2)</summary><blockquote>

`31-32`: **Enhance null check and URL rendering to support images.**

The code now handles images directly by using the appropriate render method based on the content type. However, I see a potential improvement in the null check.

```diff
-  if (!data || !data?.url && !data?.isImage) return null;
+  if (!data || (!data.url && !data.isImage)) return null;
```

The nested `?.` operator is redundant after the initial `!data` check, and adding parentheses improves readability.

---

`31-32`: **Consider adding type safety for better code quality.**

Using `any` type for `data` reduces type safety. Consider creating a proper interface for the data object to ensure correct property access and prevent potential runtime errors.

```diff
+interface SourcePreviewData {
+  url?: string;
+  isImage?: boolean;
+  title?: string;
+}

-export const FullSourcePreview = (props: any) => {
-  const { data, layoutController, metaData } = props;
+export const FullSourcePreview = (props: {
+  data: SourcePreviewData;
+  layoutController: any;
+  metaData: any;
+}) => {
+  const { data, layoutController, metaData } = props;
```

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/Messages/OutgoingMsg.tsx (2)</summary><blockquote>

`17-17`: **Add type safety for new props.**

The `attached_files` prop is typed as `any[]`, which could lead to runtime errors. Consider creating a proper interface for the file objects.

```diff
+interface AttachedFile {
+  id: string;
+  name: string;
+  type: string;
+  url: string;
+  isImage?: boolean;
+  // Add other necessary properties
+}

interface OutgoingMsgProps {
  input_prompt?: string;
  input_audio?: Blob;
  input_images?: string[];
-  input_documents?: string[];
+  input_documents?: string[]; // This prop is declared but not used
  button_pressed?: ButtonPressed;
  input_location?: {
    latitude?: number;
    longitude?: number;
  };
-  attached_files?: any[];
+  attached_files?: AttachedFile[];
}
```


Also applies to: 23-23

---

`17-17`: **Unused property in interface.**

The `input_documents` property is declared in the interface but not used in the component. Either use it or remove it.

```diff
interface OutgoingMsgProps {
  input_prompt?: string;
  input_audio?: Blob;
  input_images?: string[];
-  input_documents?: string[];
  button_pressed?: ButtonPressed;
  input_location?: {
    latitude?: number;
    longitude?: number;
  };
  attached_files?: any[];
}
```

</blockquote></details>
<details>
<summary>src/components/shared/Buttons/Button.tsx (1)</summary><blockquote>

`47-62`: **Refactor icon rendering for better maintainability.**

The current implementation has some duplication in styling logic between left and right icons. Consider refactoring for better maintainability.

```diff
- {LeftIconComponent && (
-   <div
-     className={clsx(
-       "btn-icon-left",
-       "d-flex items-center",
-       showIconOnHover && "icon-hover",
-     )}
-     style={{
-       width: "fit-content",
-       gap: "4px",
-     }}
-   >
-     <LeftIconComponent />
-     {rest.children}
-   </div>
- )}
- {!RightIconComponent && !LeftIconComponent && (rest.children)}
- {RightIconComponent && (
-   <div
-     className={clsx("d-flex items-center",
-       showIconOnHover && "icon-hover",
-     )}
-   >
-     {rest.children}
-     <span className="btn-icon-right">
-       <RightIconComponent />
-     </span>
-   </div>
- )}

+ <div
+   className={clsx(
+     "d-flex items-center",
+     showIconOnHover && "icon-hover",
+     LeftIconComponent && "btn-with-left-icon"
+   )}
+   style={LeftIconComponent ? { width: "fit-content", gap: "4px" } : undefined}
+ >
+   {LeftIconComponent && <LeftIconComponent className="btn-icon-left" />}
+   {rest.children}
+   {RightIconComponent && (
+     <span className="btn-icon-right">
+       <RightIconComponent />
+     </span>
+   )}
+ </div>
```


Also applies to: 63-75

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/Messages/helpers.tsx (2)</summary><blockquote>

`341-374`: **Avoid re-instantiating `Set` objects on every call**

`supportedMimeTypes` and `supportedExtensions` are recreated each time `isGoogleDocsEmbeddable` runs.  
These collections are immutable and could be hoisted outside the function to avoid unnecessary allocations, especially because `isGoogleDocsEmbeddable` may be called for every file-click.

```diff
-export function isGoogleDocsEmbeddable(input: string): boolean {
-  const supportedMimeTypes = new Set([
+const SUPPORTED_GOOGLE_DOC_MIME = new Set<string>([
     "application/pdf",
     ...
   ]);
-  const supportedExtensions = new Set([
+const SUPPORTED_GOOGLE_DOC_EXT = new Set<string>([
     ".pdf",
     ...
   ]);
+
+export function isGoogleDocsEmbeddable(input: string): boolean {
```

---

`367-373`: **Delimiter in MIME strings not handled**

When the input is a MIME type coming from `fetch()` or an HTTP header it can include parameters (e.g.  
`application/pdf; charset=utf-8`).  
Splitting on `";"` (or using `input.split(";")[0]`) before the `Set` lookup would make the check more robust.

</blockquote></details>
<details>
<summary>src/components/shared/Popper/Popper.tsx (1)</summary><blockquote>

`22-31`: **`getPopperPlacement` mapping for `"center"` is surprising**

When `x === "center"` the function returns `"top-start"`, which is not centred and may confuse future
callers.  
Either document the rationale or map `"center"` to `"auto"` / `"top"` / `"bottom"` depending on UX
requirements.

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/ChatInput/FilePreview.tsx (1)</summary><blockquote>

`157-237`: **Guard against files without extensions**

`file.name.split(".")[1].toUpperCase()` will throw if the filename lacks a dot.
Add a fallback to an empty string.

```diff
-  {file.name.split(".")[1].toUpperCase()}
+  {(file.name.split(".")[1] || "").toUpperCase()}
```

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/ChatInput/index.tsx (3)</summary><blockquote>

`222-240`: **Duplicate file-picker logic – extract to utility**  
`handleFileMenuClick` and `handlePhotoMenuClick` differ only in the accepted MIME string. Merging them into a single helper improves maintainability.

```diff
-const openPicker = (accept: string) => {
+const openPicker = (accept: string) => {
   const input = document.createElement("input");
   input.type = "file";
   input.accept = accept;
   input.onchange = onFileAdded;
   input.multiple = true;
   input.click();
 };
 
 const handleFileMenuClick = () => {
   setIsMenuOpen(false);
-  /* duplicated block */
-  const input = document.createElement("input");
-  …
+  openPicker(acceptedFileTypes);
 };
 
 const handlePhotoMenuClick = () => {
   setIsMenuOpen(false);
-  /* duplicated block */
-  const input = document.createElement("input");
-  …
+  openPicker(acceptedImageTypes);
 };
```

---

`25-27`: **Minor: remove superfluous spaces in `accept` strings**  
Browsers ignore surrounding spaces, but trimming them avoids accidental “ text/*” token mismatches in older user agents.

```diff
-const acceptedFileTypes = "application/*, text/*";
-const acceptedImageTypes = "image/*, video/*";
+const acceptedFileTypes = "application/*,text/*";
+const acceptedImageTypes = "image/*,video/*";
```

---

`58-60`: **Default `files` state can be an empty array**  
Initializing to `[]` instead of `null` removes the need for optional chaining (`files?.`) and narrows the type to `UploadedFile[]`, reducing `null` checks throughout the component.

```diff
-const [files, setFiles] = useState<UploadedFile[] | null>(null);
+const [files, setFiles] = useState<UploadedFile[]>([]);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d846d35be8c156e6989478758dbfc7b9b247e387 and 136e8c7dbe1671e292b7850b61eeb9bfaeffb984.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (22)</summary>

* `package.json` (1 hunks)
* `src/assets/SvgIcons/IconFile.tsx` (1 hunks)
* `src/assets/SvgIcons/IconImage.tsx` (1 hunks)
* `src/assets/SvgIcons/IconPlus.tsx` (1 hunks)
* `src/components/shared/Buttons/Button.tsx` (3 hunks)
* `src/components/shared/Buttons/IconButton.tsx` (1 hunks)
* `src/components/shared/Buttons/buttons.scss` (2 hunks)
* `src/components/shared/Loaders.tsx` (1 hunks)
* `src/components/shared/Popper/Popper.tsx` (2 hunks)
* `src/components/shared/Popper/popper.scss` (1 hunks)
* `src/components/shared/Tooltip.tsx` (2 hunks)
* `src/contexts/MessagesContext.tsx` (1 hunks)
* `src/contexts/SystemContext.tsx` (1 hunks)
* `src/css/_animations.scss` (1 hunks)
* `src/css/_extra.scss` (1 hunks)
* `src/widgets/copilot/components/ChatInput/FilePreview.tsx` (1 hunks)
* `src/widgets/copilot/components/ChatInput/chatInput.scss` (1 hunks)
* `src/widgets/copilot/components/ChatInput/index.tsx` (10 hunks)
* `src/widgets/copilot/components/Messages/OutgoingMsg.tsx` (5 hunks)
* `src/widgets/copilot/components/Messages/Sources.tsx` (2 hunks)
* `src/widgets/copilot/components/Messages/helpers.tsx` (4 hunks)
* `src/widgets/copilot/components/Messages/index.tsx` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>src/components/shared/Buttons/IconButton.tsx (1)</summary><blockquote>

<details>
<summary>src/components/shared/Buttons/Button.tsx (1)</summary>

* `ButtonProps` (8-19)

</details>

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/Messages/Sources.tsx (1)</summary><blockquote>

<details>
<summary>src/widgets/copilot/components/Messages/helpers.tsx (2)</summary>

* `renderImageInIframe` (420-451)
* `getEmbedUrl` (397-419)

</details>

</blockquote></details>
<details>
<summary>src/widgets/copilot/components/ChatInput/FilePreview.tsx (4)</summary><blockquote>

<details>
<summary>src/contexts/hooks.tsx (1)</summary>

* `useSystemContext` (6-7)

</details>
<details>
<summary>src/widgets/copilot/components/Messages/Sources.tsx (1)</summary>

* `FullSourcePreview` (19-95)

</details>
<details>
<summary>src/widgets/copilot/components/Messages/helpers.tsx (2)</summary>

* `isGoogleDocsEmbeddable` (341-396)
* `truncateMiddle` (323-339)

</details>
<details>
<summary>src/components/shared/Loaders.tsx (1)</summary>

* `CircularLoader` (1-6)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/contexts/MessagesContext.tsx</summary>

[error] 72-72: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.


(lint/performance/noDelete)

</details>

</details>
<details>
<summary>🪛 GitHub Check: CodeQL</summary>

<details>
<summary>src/widgets/copilot/components/ChatInput/FilePreview.tsx</summary>

[warning] 151-151: DOM text reinterpreted as HTML
[DOM text](1) is reinterpreted as HTML without escaping meta-characters.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (24)</summary><blockquote>

<details>
<summary>package.json (1)</summary>

`13-13`: **Addition of @popperjs/core is appropriate**

The addition of this dependency aligns with the PR's refactoring of the Popper component to use a standard positioning library instead of manual coordinate calculations, which should improve reliability and maintainability.

</details>
<details>
<summary>src/components/shared/Buttons/buttons.scss (3)</summary>

`25-27`: **Well-implemented button icon styling**

The new `.btn-icon-left` class provides proper styling for left-positioned icons within buttons.

---

`29-34`: **Button icon right positioning looks good**

The positioning for right icons maintains the appropriate absolute positioning and alignment properties.

---

`50-52`: **Appropriate visual feedback for pressed state**

The new `.depressed` class properly implements visual feedback for pressed button states using a light background color.

</details>
<details>
<summary>src/css/_animations.scss (1)</summary>

`88-103`: **Elegant CSS-based circular loader implementation**

The new CSS-based circular loader is well-implemented using modern CSS techniques (masks, gradients, aspect-ratio) and efficient animation. This approach is more maintainable than the previous SVG-based implementation and provides good browser compatibility with both standard and webkit-prefixed properties.

</details>
<details>
<summary>src/css/_extra.scss (1)</summary>

`209-209`: **Appropriate positioning context for Popper.js integration**

Adding `position: relative` establishes the necessary positioning context for the container, which is required for proper functioning of the refactored Popper component using Popper.js.

</details>
<details>
<summary>src/components/shared/Popper/popper.scss (1)</summary>

`7-7`: **Good optimization on the transition property.**

Limiting the transition to just opacity (instead of all properties) improves performance, especially when using Popper.js for positioning calculations.

</details>
<details>
<summary>src/widgets/copilot/components/ChatInput/chatInput.scss (2)</summary>

`35-37`: **Image preview dimensions look good.**

The reduction from 80px to 60px height and the change to a fixed width of 100px will create a more compact and consistent UI for file previews.

---

`44-52`: **Well-structured menu item styling.**

These styles appropriately define the interactive behavior for menu items with good spacing, cursor feedback and hover effects.

</details>
<details>
<summary>src/components/shared/Buttons/IconButton.tsx (1)</summary>

`8-8`: **Good implementation of the pressed state.**

The addition of the `isPressed` prop and the conditional "depressed" class assignment properly handles the button's visual state, keeping it consistent with other button components in the system.



Also applies to: 11-11

</details>
<details>
<summary>src/widgets/copilot/components/Messages/index.tsx (2)</summary>

`29-29`: **Properly forwarding attached_files prop.**

This addition enables file attachments to be rendered in outgoing messages, which aligns with the file upload feature implementation.

---

`59-59`: **Good defensive programming.**

Adding the null check before calling `avoidAutoplay()` prevents potential runtime errors if the function is undefined.

</details>
<details>
<summary>src/assets/SvgIcons/IconImage.tsx (1)</summary>

`3-18`: _🛠️ Refactor suggestion_

**Consider adding proper TypeScript typing instead of using `any`**

The component is using `any` type for props which reduces type safety. Consider creating a proper interface or type for the component props.

Also, unlike `IconFile.tsx`, this component doesn't spread remaining props to the SVG element, which limits customization possibilities.


```diff
-const IconImage = (props: any) => {
+interface IconImageProps {
+  size?: number;
+  [key: string]: any; // For additional props
+}
+
+const IconImage = (props: IconImageProps) => {
   const size = props.size || 16;
   return (
     <SvgIcon>
       <svg 
         xmlns="http://www.w3.org/2000/svg"
         viewBox="0 0 512 512"
         width={size}
         height={size}
+        {...props}
       >
         <path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/>
       </svg>
     </SvgIcon>
```


<details>
<summary>⛔ Skipped due to learnings</summary>

```
Learnt from: milovate
PR: GooeyAI/gooey-web-widget#17
File: src/assets/SvgIcons/LocationMarker.tsx:3-3
Timestamp: 2025-04-29T09:02:13.084Z
Learning: The SVG icon components in the GooeyAI/gooey-web-widget repository use `props: any` type definition for consistency across all icon components.
```

```
Learnt from: milovate
PR: GooeyAI/gooey-web-widget#17
File: src/assets/SvgIcons/LocationMarker.tsx:3-3
Timestamp: 2025-04-29T09:02:13.084Z
Learning: SVG icon components in the GooeyAI/gooey-web-widget repository consistently use `props: any` type pattern as a codebase convention rather than more specific TypeScript interfaces.
```

</details>

</details>
<details>
<summary>src/assets/SvgIcons/IconFile.tsx (1)</summary>

`3-18`: _🛠️ Refactor suggestion_

**Add proper TypeScript typing instead of using `any`**

The component uses `any` type for props which reduces type safety. Consider creating a proper interface or type for the component props.


```diff
-const IconFile = (props: any) => {
+interface IconFileProps {
+  size?: number;
+  [key: string]: any; // For additional props
+}
+
+const IconFile = (props: IconFileProps) => {
   const size = props.size || 16;
   return (
     <SvgIcon>
       <svg 
         xmlns="http://www.w3.org/2000/svg"
         viewBox="0 0 512 512"
         width={size}
         height={size}
         {...props}
       >
         <path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z" />
       </svg>
     </SvgIcon>
```


<details>
<summary>⛔ Skipped due to learnings</summary>

```
Learnt from: milovate
PR: GooeyAI/gooey-web-widget#17
File: src/assets/SvgIcons/LocationMarker.tsx:3-3
Timestamp: 2025-04-29T09:02:13.084Z
Learning: The SVG icon components in the GooeyAI/gooey-web-widget repository use `props: any` type definition for consistency across all icon components.
```

```
Learnt from: milovate
PR: GooeyAI/gooey-web-widget#17
File: src/assets/SvgIcons/LocationMarker.tsx:3-3
Timestamp: 2025-04-29T09:02:13.084Z
Learning: SVG icon components in the GooeyAI/gooey-web-widget repository consistently use `props: any` type pattern as a codebase convention rather than more specific TypeScript interfaces.
```

</details>

</details>
<details>
<summary>src/assets/SvgIcons/IconPlus.tsx (1)</summary>

`3-18`: _🛠️ Refactor suggestion_

**Add proper typing and ensure consistent null checking across icon components**

The component uses `any` type for props and has inconsistent null checking compared to other icon components. The other components use `||` while this one uses optional chaining `?.`.


```diff
-const IconPlus = (props: any) => {
+interface IconPlusProps {
+  size?: number;
+  [key: string]: any; // For additional props
+}
+
+const IconPlus = (props: IconPlusProps) => {
-  const size = props?.size || 10;
+  const size = props.size || 10;
   return (
     <SvgIcon>
       <svg
         xmlns="http://www.w3.org/2000/svg"
         viewBox="0 0 448 512"
         height={size}
         width={size}
         {...props}
       >
         <path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" />
       </svg>
     </SvgIcon>
```


<details>
<summary>⛔ Skipped due to learnings</summary>

```
Learnt from: milovate
PR: GooeyAI/gooey-web-widget#17
File: src/assets/SvgIcons/LocationMarker.tsx:3-3
Timestamp: 2025-04-29T09:02:13.084Z
Learning: The SVG icon components in the GooeyAI/gooey-web-widget repository use `props: any` type definition for consistency across all icon components.
```

</details>

</details>
<details>
<summary>src/contexts/SystemContext.tsx (1)</summary>

`161-161`: **Good optimization to prevent unnecessary state updates**

The added guard clause prevents unnecessary state updates when attempting to close an already closed drawer. This is a good performance optimization.

</details>
<details>
<summary>src/widgets/copilot/components/Messages/Sources.tsx (1)</summary>

`9-9`: **Imported new function for better image handling.**

The import of `renderImageInIframe` complements the existing functionality by providing a way to render images in an iframe with proper styling.

</details>
<details>
<summary>src/components/shared/Loaders.tsx (1)</summary>

`4-4`: **Simplified loader implementation using CSS-based animation.**

The CircularLoader component has been simplified to use CSS-based animation instead of SVG markup. This is a good practice that separates styling concerns from component structure.

</details>
<details>
<summary>src/widgets/copilot/components/Messages/OutgoingMsg.tsx (2)</summary>

`4-4`: **Added file attachment support to outgoing messages.**

The component now imports FilePreview and adds props for handling documents and attached files, which is consistent with the PR objective of adding file attachment functionality.



Also applies to: 17-17, 23-23

---

`64-68`: <details>
<summary>❌ Incorrect review comment</summary>

**Add target="_blank" and rel="noopener noreferrer" for security.**

When linking to external resources, it's a security best practice to include these attributes. Consider adding them if FilePreview component renders links to external content.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if FilePreview component opens links in new tabs and if it includes security attributes

# Look for the FilePreview component implementation
echo "Searching for FilePreview component implementation..."
rg -A 20 "FilePreview" src/widgets/copilot/components/ChatInput/FilePreview.tsx

# Look for any anchor tags in the FilePreview component
echo "Checking for anchor tags in FilePreview..."
rg "<a" -A 3 src/widgets/copilot/components/ChatInput/FilePreview.tsx
```

Length of output: 2101

---



</details>

**Ignore security attribute suggestion for FilePreview**

The FilePreview and FilePreviewItem components don’t render any `<a>` tags or external links—they use click handlers and a sidebar drawer instead—so there’s no need to add `target="_blank"` or `rel="noopener noreferrer"`. 

> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>src/components/shared/Buttons/Button.tsx (2)</summary>

`15-18`: **Added support for left icons and pressed state.**

The button interface now includes props for a left icon component and a pressed state, which enhances the component's flexibility.

---

`38-39`: <details>
<summary>❓ Verification inconclusive</summary>

**Changed onClick to onMouseDown with pressed state.**

Using `onMouseDown` instead of `onClick` changes the button's behavior subtly. This might be intentional to work with the new `isPressed` state, but ensure this is the desired behavior as it will trigger before the click is completed.

---

🌐 Web query:

```web
What's the difference between onClick and onMouseDown in React buttons?
```

💡 Result:

In React, both `onClick` and `onMouseDown` are event handlers used on buttons (and other elements), but they respond to different mouse interactions:

1. **onClick**: This event is triggered when the user presses and releases the mouse button over an element. It represents the full click action—mouse button down and then up, both happening on the same element.

2. **onMouseDown**: This event is triggered as soon as the mouse button is pressed down over an element, before it is released.

### Key Differences:
- **Timing**: `onMouseDown` fires immediately when the mouse button is pressed down. `onClick` fires after the mouse button is released, assuming the mouse pointer is still over the element.
- **User Interaction**: `onMouseDown` can be useful for actions that need to start as soon as the mouse button is pressed, like starting a drag operation or showing feedback. `onClick` is typically used for final action confirmation, like submitting a form or opening a modal.
- **Event sequence**: When clicking a button, `onMouseDown` fires first, followed by `onMouseUp`, and then `onClick`.

### Summary:
- Use `onClick` for normal button press interactions.
- Use `onMouseDown` if you want to respond immediately when the mouse is pressed down, before the button is released.

Would you like a brief code example demonstrating the difference?

---



</details>

**Action Required: Confirm intentional use of onMouseDown for immediate press feedback**

Using onMouseDown (which fires as soon as the button is pressed) instead of onClick (which fires after the button is released) will trigger your handler earlier in the interaction sequence. This is appropriate if you want to update `isPressed` and show the “depressed” style on mouse-down, but it may also affect keyboard or touch interactions.

Please verify that:

- You want the state change and side effects to occur on mouse-down rather than on click (mouse-up).
- This early trigger aligns with your accessibility and UX requirements.

Location:
- **File:** src/components/shared/Buttons/Button.tsx  
  **Lines:** 38–39  
  ```diff
-     onMouseDown={onClick}
+     onMouseDown={onClick}
      className={clsx(variantClasses, className, isPressed && "depressed")}
  ```

</details>
<details>
<summary>src/components/shared/Popper/Popper.tsx (1)</summary>

`51-65`: **Ensure Popper instance updates on prop changes**

`createPopper` is re-executed when `referenceElement`, `direction`, or `showModal`
change, but not when `ModalContent` height/width changes (e.g. on window resize).  
Consider adding the `update` modifier or listening to resize events to keep positioning correct.

</details>
<details>
<summary>src/widgets/copilot/components/ChatInput/index.tsx (1)</summary>

`137-154`: **Payload key for videos may be misleading**  
Videos are appended to `input_documents`, not to a dedicated `input_videos`. If the backend distinguishes videos from docs, this can break previews or downstream processing. Double-check the server contract.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:42:18Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:10Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:21Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:29Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:34Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:41Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:42Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:48Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:46:54Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:00Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:07Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:13Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:17Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:24Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:29Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:35Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:41Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:45Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:46Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:47:50Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:48:20Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:48:26Z
user: login: entelligence-ai-pr-reviews[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:48:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/widgets/copilot/components/ChatInput/index.tsx (2)</summary><blockquote>

`85-98`: _🛠️ Refactor suggestion_

**Popper auto-dismisses even when clicking inside the menu**

The click handler only checks if the click is outside the menu button, but doesn't check if it's inside the menu content. This will cause the menu to close when users try to click on the "File" or "Image or Video" buttons.



```diff
 const handleClick = (e: MouseEvent) => {
+  const popperElement = document.querySelector('.gooey-popper-content');
   if (
     menuButtonRef.current &&
-    !menuButtonRef.current.contains(e.target as Node)
+    !menuButtonRef.current.contains(e.target as Node) &&
+    !(popperElement && popperElement.contains(e.target as Node))
   ) {
     setIsMenuOpen(false);
   }
 };
```

---

`174-198`: _⚠️ Potential issue_

**Uncaught promise rejections during file upload**

The `try/catch` block around the Promise won't catch errors from the Promise itself since it's asynchronous. You need to add a `.catch()` handler to the Promise chain.



```diff
 try {
   if (!config || !config.apiUrl) return;
-  uploadFileToGooey(config.apiUrl, toUpload).then((url) => {
+  uploadFileToGooey(config.apiUrl, toUpload)
+    .then((url) => {
       setFiles((prev: any) => {
         const idx = prev.findIndex((f: any) => f.id === id);
         if (idx === -1) return prev; // if photo removed before upload completed
         const updated = [...prev];
         updated[idx] = {
           ...updated[idx],
           isUploading: false,
           gooeyUrl: url,
         };
         return updated;
       });
-  });
+    })
+    .catch((err) => {
+      console.error(err);
+      setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
+      // TODO: show error toast
+    });
 } catch (err) {
   console.error(err);
   setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
   // TODO: show error toast
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/widgets/copilot/components/ChatInput/index.tsx (2)</summary><blockquote>

`137-153`: **Review handling of video file attachments**

Videos are being added to `input_documents` instead of having their own property, which might lead to confusion. Also, there's an inefficient pattern for concatenating videos with documents.


```diff
 if (videos.length)
   payload.input_documents = [
-    ...(payload.input_documents || []),
+    ...payload.input_documents ?? [],
     ...videos.map((file) => file.gooeyUrl),
   ];
```

Consider adding a comment explaining why videos are treated as documents, or create a separate property if appropriate.

---

`223-241`: **Refactor duplicate file input code**

There's significant duplication between `handleFileMenuClick` and `handlePhotoMenuClick` methods. Consider creating a reusable function to eliminate this duplication.


```diff
+const createFileInput = (acceptType: string) => {
+  setIsMenuOpen(false);
+  const input = document.createElement("input");
+  input.type = "file";
+  input.accept = acceptType;
+  input.onchange = onFileAdded;
+  input.multiple = true;
+  input.click();
+};

 const handleFileMenuClick = () => {
-  setIsMenuOpen(false);
-  const input = document.createElement("input");
-  input.type = "file";
-  input.accept = acceptedFileTypes;
-  input.onchange = onFileAdded;
-  input.multiple = true;
-  input.click();
+  createFileInput(acceptedFileTypes);
 };

 const handlePhotoMenuClick = () => {
-  setIsMenuOpen(false);
-  const input = document.createElement("input");
-  input.type = "file";
-  input.accept = acceptedImageTypes;
-  input.onchange = onFileAdded;
-  input.multiple = true;
-  input.click();
+  createFileInput(acceptedImageTypes);
 };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 136e8c7dbe1671e292b7850b61eeb9bfaeffb984 and c3658defcbb08118d414c62af12bc59ec4f362d6.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/widgets/copilot/components/ChatInput/index.tsx` (10 hunks)
* `src/widgets/copilot/components/Messages/Sources.tsx` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/widgets/copilot/components/Messages/Sources.tsx

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>src/widgets/copilot/components/ChatInput/index.tsx (4)</summary>

`15-20`: **Good addition of new components and UUID for file management**

The addition of new icon components, popper menu, and UUID generator provides better structure for file handling and UI organization.

---

`25-37`: **Well-defined file type constants and interface**

The new `UploadedFile` interface and file type constants provide better type safety and organization. The interface properly encapsulates all necessary properties for file state management including the removal functionality.

---

`289-324`: **Good implementation of file upload menu UI**

The new file upload menu UI is a good improvement, offering clear options for different types of file uploads with appropriate icons. The popper menu implementation with direction control provides a good user experience.

---

`343-351`: **Clean button implementation with conditional rendering**

The send button implementation is clean with appropriate conditionals for different states.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:13:43Z

Review comments:
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "50%",
+            left: "50%",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <span style={{ zIndex: 2 }}>
+            <CircularLoader size={32} />
+          </span>
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            right: "-16px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-4 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile();
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <div
+        className={clsx(
+          isUploading && "uploading-box",
+          "overflow-hidden file-preview-box",
+        )}
+      >
+        <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: github-advanced-security[bot]
body: ## DOM text reinterpreted as HTML

[DOM text](1) is reinterpreted as HTML without escaping meta-characters.

[Show more details](https://github.com/GooeyAI/gooey-web-widget/security/code-scanning/58)
created_at: 2025-05-21T11:39:09Z
author_association: NONE
diff_hunk: @@ -27,8 +28,8 @@ export const FullSourcePreview = (props: any) => {
     }, 0);
   }, [data.url]);
 
-  const embedUrl = getEmbedUrl(data.url);
-  if (!data || !data?.url) return null;
+  if (!data || !data?.url && !data?.isImage) return null;
+  const embedUrl = data?.isImage ? renderImageInIframe(data.url) : getEmbedUrl(data.url);
path: src/widgets/copilot/components/Messages/Sources.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for URL processing.**

The code doesn't handle potential errors when processing URLs. Consider adding try/catch blocks to prevent runtime errors if URL processing fails.

```diff
-  const embedUrl = data?.isImage ? renderImageInIframe(data.url) : getEmbedUrl(data.url);
+  let embedUrl;
+  try {
+    embedUrl = data?.isImage ? renderImageInIframe(data.url) : getEmbedUrl(data.url);
+  } catch (error) {
+    console.error("Error processing URL:", error);
+    embedUrl = data.url; // Fallback to original URL
+  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  if (!data || !data?.url && !data?.isImage) return null;
- const embedUrl = data?.isImage ? renderImageInIframe(data.url) : getEmbedUrl(data.url);
+ let embedUrl;
+ try {
+   embedUrl = data?.isImage
+     ? renderImageInIframe(data.url)
+     : getEmbedUrl(data.url);
+ } catch (error) {
+   console.error("Error processing URL:", error);
+   embedUrl = data.url; // Fallback to original URL
+ }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/Messages/Sources.tsx around lines 31 to 32,
the code processes URLs without error handling, which can cause runtime errors
if URL processing fails. Wrap the URL processing logic inside a try/catch block
to catch any exceptions from renderImageInIframe or getEmbedUrl functions, and
handle errors gracefully, such as returning null or logging the error.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -343,10 +406,46 @@ export const getEmbedUrl = (url: string) => {
       return `https://www.youtube.com/embed/${match[1]}`;
     }
 
-    // Return original URL if not YouTube
+    if (isGoogleDocsEmbeddable(url)) {
+      return `https://docs.google.com/gview?url=${encodeURIComponent(url)}&embedded=true`;
+    }
+
+    // Return original
     return url;
   } catch (error) {
     console.error("Error processing URL:", error);
     return url;
   }
 };
+export function renderImageInIframe(imgBlobUrl: string) {
+  const html = `
+    <html>
+      <head>
+        <style>
+          html, body {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+            width: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: white;
+          }
+          img {
+            max-width: 100%;
+            max-height: 100%;
+            object-fit: contain;
+          }
+        </style>
+      </head>
+      <body>
+        <img src="${imgBlobUrl}" />
+      </body>
+    </html>
+  `;
+
+  const blob = new Blob([html], { type: 'text/html' });
+  const blobUrl = URL.createObjectURL(blob);
+  return blobUrl;
+}
path: src/widgets/copilot/components/Messages/helpers.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Potential Blob URL leak in `renderImageInIframe`**

`URL.createObjectURL` reserves memory until `URL.revokeObjectURL` is invoked.  
`renderImageInIframe` returns the generated URL but never revokes it, so every preview opened leaves a dangling
object-URL in the browser.

Add a small helper or documentation note so callers can revoke the URL during component unmount / drawer close.

```diff
-const blobUrl = URL.createObjectURL(blob);
-return blobUrl;
+const blobUrl = URL.createObjectURL(blob);
+
+// Call-site should invoke:  URL.revokeObjectURL(blobUrl)  when iframe is disposed.
+return blobUrl;
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export function renderImageInIframe(imgBlobUrl: string) {
  const html = `
    <html>
      <head>
        <style>
          html, body {
            margin: 0;
            padding: 0;
            height: 100%;
            width: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            background: white;
          }
          img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
          }
        </style>
      </head>
      <body>
        <img src="${imgBlobUrl}" />
      </body>
    </html>
  `;

  const blob = new Blob([html], { type: 'text/html' });
-  const blobUrl = URL.createObjectURL(blob);
-  return blobUrl;
+  const blobUrl = URL.createObjectURL(blob);
+
+  // Call-site should invoke: URL.revokeObjectURL(blobUrl) when iframe is disposed.
+  return blobUrl;
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/Messages/helpers.tsx around lines 420 to 451,
the function renderImageInIframe creates a Blob URL using URL.createObjectURL
but never revokes it, causing a memory leak. To fix this, add a comment or
helper function advising callers to call URL.revokeObjectURL on the returned URL
when the preview is closed or the component unmounts, ensuring proper cleanup of
the Blob URL.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Revoke temporary Object URLs**

`URL.createObjectURL(data)` is created on every render but never revoked.  
This leaks memory, especially when users attach large files.

Add a `useEffect` inside `ImagePreviewItem` and `FilePreviewItem` (or wrap URL creation in
`useMemo`) to revoke the URL on unmount.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/ChatInput/FilePreview.tsx around lines 50 to
55, the code creates temporary object URLs with URL.createObjectURL(data) on
every render but does not revoke them, causing memory leaks. To fix this, move
the URL creation inside a useEffect or useMemo hook within the ImagePreviewItem
and FilePreviewItem components, and add cleanup logic to revoke the URL using
URL.revokeObjectURL when the component unmounts or the file data changes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**JSON files incorrectly treated as images**

`handleFileClick` sets `isImage: true` for files whose MIME type contains `"json"`.  
`FullSourcePreview` will therefore attempt to render JSON inside an `<img>` tag and fail.

```diff
-  file?.data?.type?.includes("json") ||
-  file?.data?.type?.includes("image")
+  file?.data?.type?.includes("image")
```

For JSON, consider opening in a new tab or showing formatted text.




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  const handleFileClick = (file: any) => {
    if (isGoogleDocsEmbeddable(file?.data?.type)) {
      openInSidebar({ url: file.gooeyUrl, title: file.name });
    } else if (
      file?.data?.type?.includes("image")
    ) {
      openInSidebar({
        url: URL.createObjectURL(file.data),
        title: file.name,
        isImage: true,
      });
    } else {
      window.open(URL.createObjectURL(file.data), "_blank");
    }
  };
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/ChatInput/FilePreview.tsx around lines 28 to
43, the handleFileClick function incorrectly sets isImage: true for JSON files,
causing JSON to be rendered as an image and fail. To fix this, separate the
handling of JSON files from images: for JSON files, open them in a new tab or
display formatted text instead of setting isImage to true, and only set isImage:
true for actual image MIME types.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "50%",
+            left: "50%",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <span style={{ zIndex: 2 }}>
+            <CircularLoader size={32} />
+          </span>
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            right: "-16px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-4 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile();
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <div
+        className={clsx(
+          isUploading && "uploading-box",
+          "overflow-hidden file-preview-box",
+        )}
+      >
+        <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
+      </div>
+    </div>
+  );
+};
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`alt` attribute uses undefined `name`**

`<img src={fileURL} alt={`preview-${name}`}>` references a non-existent `name` variable.  
It falls back to the global `window.name` property at runtime.

```diff
-<img src={fileURL} alt={`preview-${name}`} ...
+<img src={fileURL} alt={`preview-${fileURL.split('/').pop()}`} ...
```
Or better, pass the file name as prop.




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
      <div
        className={clsx(
          isUploading && "uploading-box",
          "overflow-hidden file-preview-box",
        )}
      >
-       <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
+       <img
+         src={fileURL}
+         alt={`preview-${fileURL.split('/').pop()}`}
+         className="br-large b-1"
+       />
      </div>
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: CodeQL</summary>

[warning] 151-151: DOM text reinterpreted as HTML
[DOM text](1) is reinterpreted as HTML without escaping meta-characters.

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/ChatInput/FilePreview.tsx between lines 90 and
155, the img element's alt attribute references an undefined variable 'name',
causing it to fallback to window.name. To fix this, add a 'name' prop to the
ImagePreviewItem component's props and use it in the alt attribute as
`alt={`preview-${name}`}`. Ensure the parent component passes the correct file
name to this prop.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,26 +71,44 @@ const ChatInput = () => {
       if (typeof image === "string") return; // @TODO: support for CDN URLs
       // check if image is { name: string; mime: string; bytes: number[]; };
       if (typeof image === "object" && image.bytes) {
-        const fileObj = new File([new Uint8Array(image.bytes)], image.name || "gooey-image.png", { type: image.mime || "image/png" });
+        const fileObj = new File(
+          [new Uint8Array(image.bytes)],
+          image.name || "gooey-image.png",
+          { type: image.mime || "image/png" },
+        );
         setFiles(processFiles([fileObj]));
         setPreAttachedFileUsed(true);
       }
     });
-
   }, [config?.payload?.input_images, preAttachedFileUsed]);
 
+  // Dismiss menu on outside click
+  useEffect(() => {
+    if (!isMenuOpen) return;
+    const handleClick = (e: MouseEvent) => {
+      if (
+        menuButtonRef.current &&
+        !menuButtonRef.current.contains(e.target as Node)
+      ) {
+        setIsMenuOpen(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClick);
+    return () => document.removeEventListener("mousedown", handleClick);
+  }, [isMenuOpen]);
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Popper auto-dismisses even when clicking inside the menu**  
`handleClick` only checks `menuButtonRef.current.contains(e.target)`; clicks inside the Popper content itself will close the menu because they fall outside the button.  
Consider also excluding the popper container (or stop-propagation inside the menu) so users can interact with the “File / Image or Video” buttons without the menu disappearing first.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/ChatInput/index.tsx around lines 85 to 98, the
click handler only checks if the click is outside the menu button but does not
consider clicks inside the Popper menu content, causing the menu to close when
interacting with it. Update the handleClick function to also check if the click
target is inside the Popper container element (add a ref for the Popper
container if needed) and prevent closing the menu if the click is within either
the button or the Popper content.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,30 +172,40 @@
   };
 
   const processFiles = (files: Array<any>) => {
-    return files.map((file: any, index: number) => {
+    return files.map((file: any) => {
+      const id = uuidv4();
       makeFileBuffer(file).then((blob) => {
         const toUpload = new File([blob as Blob], file.name);
-        uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
-          setFiles((prev: any) => {
-            if (!prev[index]) return prev; // if photo removed before upload completed
-            prev[index].isUploading = false;
-            prev[index].gooeyUrl = url;
-            return [...prev];
+        try {
+          uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
+            setFiles((prev: any) => {
+              const idx = prev.findIndex((f: any) => f.id === id);
+              if (idx === -1) return prev; // if photo removed before upload completed
+              const updated = [...prev];
+              updated[idx] = {
+                ...updated[idx],
+                isUploading: false,
+                gooeyUrl: url,
+              };
+              return updated;
+            });
           });
-        });
+        } catch (err) {
+          console.error(err);
+          setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
+          // TODO: show error toast
+        }
       });
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Uncaught promise rejections during file upload**  
`uploadFileToGooey(...).then(...);` is wrapped in a `try {}` block, but `try/catch` cannot capture errors thrown asynchronously. A network failure will therefore trigger an unhandled promise rejection and leave `isUploading` stuck at `true`.  
Patch:

```diff
-uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
+uploadFileToGooey(config!.apiUrl!, toUpload)
+  .then((url) => {
     /* …success logic… */
-});
+  })
+  .catch((err) => {
+    console.error(err);
+    setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
+    // TODO: surface a toast / snackbar
+  });
```
  


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
makeFileBuffer(file).then((blob) => {
  const toUpload = new File([blob as Blob], file.name);
  try {
    uploadFileToGooey(config!.apiUrl!, toUpload)
      .then((url) => {
        setFiles((prev: any) => {
          const idx = prev.findIndex((f: any) => f.id === id);
          if (idx === -1) return prev; // if photo removed before upload completed
          const updated = [...prev];
          updated[idx] = {
            ...updated[idx],
            isUploading: false,
            gooeyUrl: url,
          };
          return updated;
        });
      })
      .catch((err) => {
        console.error(err);
        setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
        // TODO: surface a toast / snackbar
      });
  } catch (err) {
    console.error(err);
    setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
    // TODO: show error toast
  }
});
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/widgets/copilot/components/ChatInput/index.tsx around lines 174 to 198,
the uploadFileToGooey call is inside a try/catch block but uses .then for async
handling, so errors from the promise are not caught and cause unhandled
rejections. To fix this, replace the .then chain with async/await inside an
async function or add a .catch handler to the promise to properly handle errors,
update state accordingly, and prevent isUploading from remaining true
indefinitely.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T11:42:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconFile = (props: any) => {
+  const size = props.size || 16;
+  return (
+    <SvgIcon>
+      <svg 
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 512 512"
+        width={size}
+        height={size}
+        {...props}
+      >
+        <path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z" />
+      </svg>
path: src/assets/SvgIcons/IconFile.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Accessibility**: The SVG icon lacks accessibility attributes. SVGs should include a `title` element or appropriate ARIA attributes to ensure screen readers can properly describe the icon to users with visual impairments.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Alternative text title element cannot be empty
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
      <svg 
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 512 512"
        width={size}
        height={size}
        role="img"
        aria-label="File"
        {...props}
      >
        <title>File</title>
        <path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z" />
      </svg>
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconImage = (props: any) => {
+  const size = props.size || 16;
+  return (
+    <SvgIcon>
+      <svg 
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 512 512"
+        width={size}
+        height={size}
+      >
+        <path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/>
+      </svg>
path: src/assets/SvgIcons/IconImage.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The SVG element lacks a `title` element or appropriate ARIA attributes, making it inaccessible to screen readers. This violates accessibility standards for SVG images.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Alternative text title element cannot be empty
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
      <svg 
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 512 512"
        width={size}
        height={size}
        role="img"
        aria-label="Image icon"
      >
        <title>Image icon</title>
        <path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/>
      </svg>
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconPlus = (props: any) => {
+  const size = props?.size || 10;
+  return (
+    <SvgIcon>
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 448 512"
+        height={size}
+        width={size}
+        {...props}
+      >
+        <path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" />
+      </svg>
path: src/assets/SvgIcons/IconPlus.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Accessibility**: The SVG icon lacks accessibility features. Adding a `<title>` element or appropriate ARIA attributes will make the icon accessible to screen readers.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Alternative text title element cannot be empty
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 448 512"
        height={size}
        width={size}
        {...props}
        aria-hidden="true"
      >
        <title>Plus</title>
        <path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" />
      </svg>
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import SvgIcon from "src/components/shared/SvgIcon";
+
+const IconPlus = (props: any) => {
+  const size = props?.size || 10;
+  return (
+    <SvgIcon>
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 448 512"
+        height={size}
+        width={size}
+        {...props}
path: src/assets/SvgIcons/IconPlus.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `{...props}` spread is applied after specific props like `height` and `width`, which means any height/width values in props would override the calculated values.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
        {...props}
        height={size}
        width={size}
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,32 +1,6 @@
-export const SkeletonBox = () => {
-  return (
-    <div
-      className="loading-skeleton bg-grey"
-      style={{ width: "100px", height: "100px" }}
-    ></div>
-  );
-};
-
 export const CircularLoader = (props: any) => {
   const size = props.size || 16;
   return (
-    <div className="circular-loader">
-      <svg
-        className="circular"
-        viewBox="25 25 50 50"
-        height={size}
-        width={size}
-      >
-        <circle
-          className="path"
-          cx="50"
-          cy="50"
-          r="20"
-          fill="none"
-          stroke-width="2"
-          stroke-miterlimit="10"
-        ></circle>
-      </svg>
-    </div>
+    <div className="circular-loader" style={{ width: size, height: size }} />
path: src/components/shared/Loaders.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `CircularLoader` component now renders a simple div without animation elements. The SVG animation was removed but the CSS class `circular-loader` likely still contains animation styles that won't work with just a div.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
    <div className="circular-loader">
        <svg
            className="circular"
            viewBox="25 25 50 50"
            height={size}
            width={size}
        >
            <circle
                className="path"
                cx="50"
                cy="50"
                r="20"
                fill="none"
                strokeWidth="2"
                strokeMiterlimit="10"
            ></circle>
        </svg>
    </div>
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,129 +13,95 @@ type PopperDirection = {
 
 interface PopperProps extends React.HTMLAttributes<HTMLDivElement> {
   children: React.ReactNode;
-  ModalContent?: any;
+  ModalContent?: () => React.ReactNode;
   direction?: PopperDirection;
   showModal: boolean;
   ModalProps?: Record<string, any>;
 }
 
-const getModalCoordinates = (
-  containerRef: HTMLElement,
-  direction: PopperDirection
-) => {
-  const containerCoordinates = containerRef.getBoundingClientRect();
-  let translateX = "0px";
-  let translateY = "0px";
-  const x = direction.x;
-  const y = direction.y;
-  const modalCoordinates = {
-    top: 0,
-    left: 0,
-    transform: "none",
-  };
-  switch (x) {
-    case "left":
-      modalCoordinates.left =
-        containerCoordinates.left - containerCoordinates.width;
-      translateX = "calc(-50% - 12px)";
-      break;
-    case "right":
-      modalCoordinates.left = containerCoordinates.right;
-      translateX = "12px";
-      break;
-    case "center":
-      modalCoordinates.left =
-        containerCoordinates.left + containerCoordinates.width / 2;
-      modalCoordinates.transform = "translate(-50%, 12px)";
-      translateY = "12px";
-      translateX = "-50%";
-      break;
-  }
-
-  switch (y) {
-    case "top":
-      modalCoordinates.top = containerCoordinates.top - 12;
-      if (x === "center") modalCoordinates.transform = "translate(-50%, -100%)";
-      else modalCoordinates.transform = "translate(0, -100%)";
-      translateY = "0";
-      if (translateX === "0") translateX = "-100%";
-      break;
-    case "bottom":
-      modalCoordinates.top = containerCoordinates.bottom;
-      break;
-    case "center":
-      modalCoordinates.top =
-        containerCoordinates.top + containerCoordinates.height / 2;
-      translateY = "-50%";
-      if (translateX === "0") translateX = "12px";
-      break;
-  }
-  modalCoordinates.transform = `translate(${translateX}, ${translateY})`;
-  return modalCoordinates;
+// Map your direction prop to Popper.js placement
+const getPopperPlacement = (direction: PopperDirection): Placement => {
+  const { x, y } = direction;
+  if (y === "top") return x === "left" ? "top-start" : x === "right" ? "top-end" : "top";
+  if (y === "bottom") return x === "left" ? "bottom-start" : x === "right" ? "bottom-end" : "bottom";
+  if (x === "left") return "left";
+  if (x === "right") return "right";
+  if (x === "center") return "top-start";
+  return "bottom";
 };
 
 const Modal = ({
-  containerRef,
+  referenceElement,
   direction,
-  style,
-  className = "",
   ModalContent,
-  ...restProps
+  style: styleProp,
+  className = "",
+  showModal,
+  ...rest
 }: {
-  ModalContent: any;
-  containerRef: HTMLElement | null;
+  ModalContent: () => React.ReactNode;
+  referenceElement: HTMLElement | null;
   direction: PopperDirection;
   style?: React.CSSProperties;
   className?: string;
+  showModal: boolean;
 }) => {
-  if (!containerRef) return null;
-  const modalCoordinates = getModalCoordinates(containerRef, direction);
-  if (!ModalContent) return null;
+  const popperRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (referenceElement && popperRef.current && showModal) {
+      const instance = createPopper(referenceElement, popperRef.current, {
+        placement: getPopperPlacement(direction),
+        modifiers: [
+          { name: "preventOverflow", options: { padding: 8 } },
+          { name: "flip", options: { fallbackPlacements: ["top", "bottom", "right", "left"] } },
+        ],
+      });
+      return () => {
+        instance.destroy();
+      };
+    }
+  }, [referenceElement, direction, showModal]);
+
+  if (!referenceElement || !ModalContent || !showModal) return null;
   return (
     <div
-      className={(className += " gooey-modal")}
-      style={{
-        ...style,
-        ...modalCoordinates,
-      }}
-      {...restProps}
+      ref={popperRef}
+      className={`gooey-modal ${className}`.trim()}
+      style={styleProp}
+      {...rest}
     >
       {ModalContent()}
     </div>
   );
 };
 
-const GooeyPopper = (props: PopperProps) => {
-  const {
-    ModalContent = () => null,
-    children,
-    direction = {
-      x: "center",
-      y: "bottom",
-    },
-    showModal,
-    ModalProps,
-    ...restProps
-  } = props;
-
-  const [refContainer, setRefContainer] = useState<HTMLElement | null>(null);
+const GooeyPopper = ({
+  ModalContent = () => null,
+  children,
+  direction = { x: "center", y: "bottom" },
+  showModal,
+  ModalProps,
+  ...rest
+}: PopperProps) => {
+  const refContainer = useRef<HTMLDivElement>(null);
   return (
     <div
       className="gooey-clipping-container"
-      ref={setRefContainer}
-      {...restProps}
+      ref={refContainer}
+      {...rest}
     >
       {children}
       {showModal &&
         createPortal(
           <Modal
-            containerRef={refContainer}
+            referenceElement={refContainer.current}
             direction={direction}
-            ModalContent={ModalContent as React.FC<any>}
+            ModalContent={ModalContent}
+            showModal={showModal}
             {...ModalProps}
           />,
-          gooeyShadowRoot?.querySelector(".gooey-embed-container") ||
-            (document.body as Element)
+          gooeyShadowRoot?.querySelector(".gooey-embed-container") || document.body
path: src/components/shared/Popper/Popper.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `gooeyShadowRoot` variable is used but not defined anywhere in the file, which will cause a runtime error when the code tries to access this undefined variable.


created_at: 2025-05-21T11:46:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "50%",
+            left: "50%",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <span style={{ zIndex: 2 }}>
+            <CircularLoader size={32} />
+          </span>
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            right: "-16px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-4 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile();
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <div
+        className={clsx(
+          isUploading && "uploading-box",
+          "overflow-hidden file-preview-box",
+        )}
+      >
+        <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: Missing `name` variable in `ImagePreviewItem` component causes a runtime error when rendering the `alt` attribute for the image.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
        <img src={fileURL} alt="preview-image" className={"br-large b-1"} />
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "50%",
+            left: "50%",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <span style={{ zIndex: 2 }}>
+            <CircularLoader size={32} />
+          </span>
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            right: "-16px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-4 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile();
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <div
+        className={clsx(
+          isUploading && "uploading-box",
+          "overflow-hidden file-preview-box",
+        )}
+      >
+        <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
+      </div>
+    </div>
+  );
+};
+
+export const FilePreviewItem = ({
+  file,
+  removeFile = () => {},
+  onClick,
+  isUploading,
+  isRemovable,
+}: {
+  file: any;
+  removeFile?: (e: SyntheticEvent) => void;
+  onClick: (e: SyntheticEvent) => void;
+  isUploading: boolean;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx(
+        "b-1 gp-10 br-default pos-relative d-flex align-center cr-pointer",
+      )}
+      style={{ width: "200px" }}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "0",
+            left: "0",
+            width: "100%",
+            height: "100%",
+            backgroundColor: "rgba(0, 0, 0, 0.5)",
+          }}
+          className="d-flex br-default align-center justify-center"
+        >
+          <CircularLoader size={32} />
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            left: "4px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 4,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-2 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile(e);
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <IconFile size={24} style={{ minWidth: "fit-content" }} />
+      {/* remove extension */}
+      <div
+        style={{
+          width: "90%",
+          overflow: "hidden",
+          textOverflow: "ellipsis",
+          whiteSpace: "nowrap",
+        }}
+      >
+        <p
+          className="font_14_500"
+          style={{ width: "90%", textOverflow: "ellipsis" }}
+        >
+          {truncateMiddle(file.name.split(".")[0], 20)}
+        </p>
+        <p className="font_12_400 text-muted">
+          {file.name.split(".")[1].toUpperCase()}
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: Potential runtime error when accessing file extension if filename doesn't contain a period. The code assumes all filenames have an extension.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
          {truncateMiddle(file.name.includes(".") ? file.name.split(".")[0] : file.name, 20)}
        </p>
        <p className="font_12_400 text-muted">
          {file.name.includes(".") ? file.name.split(".")[1].toUpperCase() : ""}
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:46:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: Missing keyboard accessibility in clickable elements. Components with `onClick` should also have keyboard event handlers for accessibility.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
    <div
      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
      onClick={onClick}
      onKeyDown={(e) => e.key === 'Enter' && onClick(e)}
      tabIndex={0}
      role="button"
    >
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             ) : (
-              <div>
-                <p>{file.name}</p>
-              </div>
+              <FilePreviewItem
+                file={file}
+                removeFile={(e) => {
+                  e.stopPropagation();
+                  e.preventDefault();
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                onClick={() => handleFileClick(file)}
+                isUploading={isUploading}
+                isRemovable={isRemovable}
+              />
             )}
           </div>
         );
       })}
+      <div className="gooey-scroll-fade"></div>
     </div>
   );
 };
 
+const ImagePreviewItem = ({
+  removeFile,
+  fileURL,
+  isUploading,
+  onClick,
+  isRemovable,
+}: {
+  removeFile: () => void;
+  fileURL: string;
+  isUploading: boolean;
+  onClick: (e: any) => void;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx("file-preview-box br-large pos-relative cr-pointer")}
+      onClick={onClick}
+    >
+      {isUploading && (
+        <div
+          style={{
+            position: "absolute",
+            top: "50%",
+            left: "50%",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <span style={{ zIndex: 2 }}>
+            <CircularLoader size={32} />
+          </span>
+        </div>
+      )}
+      {isRemovable && (
+        <div
+          style={{
+            position: "absolute",
+            top: "6px",
+            right: "-16px",
+            transform: "translate(-50%, -50%)",
+            zIndex: 1,
+          }}
+        >
+          <IconButton
+            className="bg-white gp-4 b-1"
+            onClick={(e) => {
+              e.stopPropagation();
+              e.preventDefault();
+              removeFile();
+            }}
+          >
+            <IconClose size={12} />
+          </IconButton>
+        </div>
+      )}
+      <div
+        className={clsx(
+          isUploading && "uploading-box",
+          "overflow-hidden file-preview-box",
+        )}
+      >
+        <img src={fileURL} alt={`preview-${name}`} className={"br-large b-1"} />
+      </div>
+    </div>
+  );
+};
+
+export const FilePreviewItem = ({
+  file,
+  removeFile = () => {},
+  onClick,
+  isUploading,
+  isRemovable,
+}: {
+  file: any;
+  removeFile?: (e: SyntheticEvent) => void;
+  onClick: (e: SyntheticEvent) => void;
+  isUploading: boolean;
+  isRemovable: boolean;
+}) => {
+  return (
+    <div
+      className={clsx(
+        "b-1 gp-10 br-default pos-relative d-flex align-center cr-pointer",
+      )}
+      style={{ width: "200px" }}
+      onClick={onClick}
+    >
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: Missing keyboard accessibility in `FilePreviewItem` component. Elements with `onClick` should also have keyboard event handlers.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
    <div
      className={clsx(
        "b-1 gp-10 br-default pos-relative d-flex align-center cr-pointer",
      )}
      style={{ width: "200px" }}
      onClick={onClick}
      onKeyDown={(e) => e.key === 'Enter' && onClick(e)}
      tabIndex={0}
      role="button"
    >
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: Using array index as React key can lead to rendering issues when items are reordered or removed. A more stable unique identifier should be used.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Avoid using the index of an array as key property in an element.
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
          <div key={`file-${file.name}-${index}`}>
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,72 +1,238 @@
 import clsx from "clsx";
 import IconClose from "src/assets/SvgIcons/IconClose";
-import Button from "src/components/shared/Buttons/Button";
+import IconFile from "src/assets/SvgIcons/IconFile";
+
+import IconButton from "src/components/shared/Buttons/IconButton";
 import { CircularLoader } from "src/components/shared/Loaders";
+import { FullSourcePreview } from "../Messages/Sources";
+import { useSystemContext } from "src/contexts/hooks";
+import { SyntheticEvent } from "react";
+import { isGoogleDocsEmbeddable, truncateMiddle } from "../Messages/helpers";
 
-const FilePreview = ({ files }: { files: Array<any> }) => {
+const FilePreview = ({
+  files,
+  isRemovable = false,
+}: {
+  files: Array<any>;
+  isRemovable?: boolean;
+}) => {
   if (!files) return null;
+  const { layoutController } = useSystemContext();
+
+  const openInSidebar = (file: any) => {
+    layoutController?.toggleSecondaryDrawer?.(() => (
+      <FullSourcePreview data={file} layoutController={layoutController} />
+    ));
+  };
+
+  const handleFileClick = (file: any) => {
+    if (isGoogleDocsEmbeddable(file?.data?.type)) {
+      openInSidebar({ url: file.gooeyUrl, title: file.name });
+    } else if (
+      file?.data?.type?.includes("json") ||
+      file?.data?.type?.includes("image")
+    ) {
+      openInSidebar({
+        url: URL.createObjectURL(file.data),
+        title: file.name,
+        isImage: true,
+      });
+    } else {
+      window.open(URL.createObjectURL(file.data), "_blank");
+    }
+  };
+
   return (
-    <div className="d-flex" style={{ gap: "12px", flexWrap: "wrap" }}>
+    <div
+      className="d-flex overflow-scroll gooey-scroll-container"
+      style={{ gap: "12px", flexWrap: "nowrap", scrollbarWidth: "thin" }}
+    >
       {files.map((file, index) => {
-        const { isUploading, name, data, removeFile } = file;
+        const { isUploading, data, removeFile } = file;
         const fileURL = URL.createObjectURL(data);
         const fileType = file.type.split("/")[0];
 
         return (
-          <div key={index} className="d-flex">
+          <div key={index}>
             {fileType === "image" ? (
-              <div className={clsx("file-preview-box br-large pos-relative")}>
-                {isUploading && (
-                  <div
-                    style={{
-                      position: "absolute",
-                      top: "50%",
-                      left: "50%",
-                      transform: "translate(-50%, -50%)",
-                      zIndex: 1,
-                    }}
-                  >
-                    <CircularLoader size={32} />
-                  </div>
-                )}
-                <div
-                  style={{
-                    position: "absolute",
-                    top: "6px",
-                    right: "-16px",
-                    transform: "translate(-50%, -50%)",
-                    zIndex: 1,
-                  }}
-                >
-                  <Button className="bg-white gp-4 b-1" onClick={removeFile}>
-                    <IconClose size={12} />
-                  </Button>
-                </div>
-                <div
-                  className={clsx(
-                    isUploading && "uploading-box",
-                    "overflow-hidden file-preview-box"
-                  )}
-                >
-                  <a href={fileURL} target="_blank">
-                    <img
-                      src={fileURL}
-                      alt={`preview-${name}`}
-                      className={"br-large b-1"}
-                    />
-                  </a>
-                </div>
-              </div>
+              <ImagePreviewItem
+                removeFile={() => {
+                  layoutController?.toggleSecondaryDrawer?.(null);
+                  removeFile();
+                }}
+                fileURL={fileURL}
+                onClick={() => handleFileClick(file)}
path: src/widgets/copilot/components/ChatInput/FilePreview.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: Type mismatch in `onClick` prop for `ImagePreviewItem`. The component expects `(e: any) => void` but `handleFileClick` doesn't accept parameters.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
                onClick={(e) => handleFileClick(file)}
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,26 +71,44 @@ const ChatInput = () => {
       if (typeof image === "string") return; // @TODO: support for CDN URLs
       // check if image is { name: string; mime: string; bytes: number[]; };
       if (typeof image === "object" && image.bytes) {
-        const fileObj = new File([new Uint8Array(image.bytes)], image.name || "gooey-image.png", { type: image.mime || "image/png" });
+        const fileObj = new File(
+          [new Uint8Array(image.bytes)],
+          image.name || "gooey-image.png",
+          { type: image.mime || "image/png" },
+        );
         setFiles(processFiles([fileObj]));
         setPreAttachedFileUsed(true);
       }
     });
-
   }, [config?.payload?.input_images, preAttachedFileUsed]);
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `useEffect` hook is missing `files?.length` in its dependency array, which could cause stale state issues when files are added or removed.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
This hook does not specify all of its dependencies: files?.length
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
  useEffect(() => {
    const input_images = config?.payload?.input_images || [];
    if (!input_images?.length || preAttachedFileUsed || files?.length) return;

    input_images.forEach((image: any) => {
      if (typeof image === "string") return; // @TODO: support for CDN URLs
      // check if image is { name: string; mime: string; bytes: number[]; };
      if (typeof image === "object" && image.bytes) {
        const fileObj = new File(
          [new Uint8Array(image.bytes)],
          image.name || "gooey-image.png",
          { type: image.mime || "image/png" },
        );
        setFiles(processFiles([fileObj]));
        setPreAttachedFileUsed(true);
      }
    });
  }, [config?.payload?.input_images, preAttachedFileUsed, files?.length]);
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,26 +71,44 @@ const ChatInput = () => {
       if (typeof image === "string") return; // @TODO: support for CDN URLs
       // check if image is { name: string; mime: string; bytes: number[]; };
       if (typeof image === "object" && image.bytes) {
-        const fileObj = new File([new Uint8Array(image.bytes)], image.name || "gooey-image.png", { type: image.mime || "image/png" });
+        const fileObj = new File(
+          [new Uint8Array(image.bytes)],
+          image.name || "gooey-image.png",
+          { type: image.mime || "image/png" },
+        );
         setFiles(processFiles([fileObj]));
         setPreAttachedFileUsed(true);
       }
     });
-
   }, [config?.payload?.input_images, preAttachedFileUsed]);
 
+  // Dismiss menu on outside click
+  useEffect(() => {
+    if (!isMenuOpen) return;
+    const handleClick = (e: MouseEvent) => {
+      if (
+        menuButtonRef.current &&
+        !menuButtonRef.current.contains(e.target as Node)
+      ) {
+        setIsMenuOpen(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClick);
+    return () => document.removeEventListener("mousedown", handleClick);
+  }, [isMenuOpen]);
+
   const resetHeight = () => {
     const ele: HTMLElement | null = inputRef.current;
     ele!.style!.height = INPUT_HEIGHT + "px";
   };
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `resetHeight` function uses non-null assertions (`!`) on potentially null values, which could cause runtime errors if `inputRef.current` is null.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Forbidden non-null assertion.
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
  const resetHeight = () => {
    const ele: HTMLElement | null = inputRef.current;
    if (ele && ele.style) ele.style.height = INPUT_HEIGHT + "px";
  };
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +124,7 @@ const ChatInput = () => {
     if (ele!.scrollHeight > INPUT_HEIGHT)
       ele?.setAttribute(
         "style",
-        "height:" + ele.scrollHeight + "px !important"
+        "height:" + ele.scrollHeight + "px !important",
       );
   };
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `handleChangeLine` function uses non-null assertion on `ele!.scrollHeight` which could cause runtime errors if `inputRef.current` is null.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Forbidden non-null assertion.
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
  const handleChangeLine = () => {
    // increase height by 24px
    const ele: HTMLElement | null = inputRef.current;
    if (ele && ele.scrollHeight > INPUT_HEIGHT)
      ele.setAttribute(
        "style",
        "height:" + ele.scrollHeight + "px !important",
      );
  };
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,30 +172,40 @@ const ChatInput = () => {
   };
 
   const processFiles = (files: Array<any>) => {
-    return files.map((file: any, index: number) => {
+    return files.map((file: any) => {
+      const id = uuidv4();
       makeFileBuffer(file).then((blob) => {
         const toUpload = new File([blob as Blob], file.name);
-        uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
-          setFiles((prev: any) => {
-            if (!prev[index]) return prev; // if photo removed before upload completed
-            prev[index].isUploading = false;
-            prev[index].gooeyUrl = url;
-            return [...prev];
+        try {
+          uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `uploadFileToGooey` call uses non-null assertions on `config!.apiUrl!` which could cause runtime errors if `config` or `config.apiUrl` is null.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
Forbidden non-null assertion.
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
        try {
          if (!config || !config.apiUrl) return;
          uploadFileToGooey(config.apiUrl, toUpload).then((url) => {
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,7 +134,23 @@ const ChatInput = () => {
       input_prompt: value.trim(),
     };
     if (files?.length) {
-      payload.input_images = files.map((file) => file.gooeyUrl);
+      const documents = files.filter(
+        (file) => file.type === "application" || file.type === "text",
+      );
+      const images = files.filter((file) => file.type === "image");
+      const videos = files.filter((file) => file.type === "video");
+
+      // attach to payload for server to process
+      if (documents.length)
+        payload.input_documents = documents.map((file) => file.gooeyUrl);
+      if (images.length)
+        payload.input_images = images.map((file) => file.gooeyUrl);
+      if (videos.length)
+        payload.input_documents = [
+          ...(payload.input_documents || []),
+          ...videos.map((file) => file.gooeyUrl),
+        ];
+      payload.attached_files = [...files];
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `payload.attached_files = [...files]` line assigns files to the payload without checking if `files` is null, which could cause runtime errors.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
      payload.attached_files = files ? [...files] : [];
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,30 +172,40 @@ const ChatInput = () => {
   };
 
   const processFiles = (files: Array<any>) => {
-    return files.map((file: any, index: number) => {
+    return files.map((file: any) => {
+      const id = uuidv4();
       makeFileBuffer(file).then((blob) => {
         const toUpload = new File([blob as Blob], file.name);
-        uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
-          setFiles((prev: any) => {
-            if (!prev[index]) return prev; // if photo removed before upload completed
-            prev[index].isUploading = false;
-            prev[index].gooeyUrl = url;
-            return [...prev];
+        try {
+          uploadFileToGooey(config!.apiUrl!, toUpload).then((url) => {
+            setFiles((prev: any) => {
+              const idx = prev.findIndex((f: any) => f.id === id);
+              if (idx === -1) return prev; // if photo removed before upload completed
+              const updated = [...prev];
+              updated[idx] = {
+                ...updated[idx],
+                isUploading: false,
+                gooeyUrl: url,
+              };
+              return updated;
+            });
           });
-        });
+        } catch (err) {
+          console.error(err);
+          setFiles((prev: any) => prev.filter((f: any) => f.id !== id));
+          // TODO: show error toast
+        }
path: src/widgets/copilot/components/ChatInput/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The error handling in the file upload process doesn't provide user feedback. The TODO comment indicates this is a known issue that needs to be addressed.


created_at: 2025-05-21T11:47:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,8 +28,8 @@ export const FullSourcePreview = (props: any) => {
     }, 0);
   }, [data.url]);
 
-  const embedUrl = getEmbedUrl(data.url);
-  if (!data || !data?.url) return null;
+  if (!data || !data?.url && !data?.isImage) return null;
path: src/widgets/copilot/components/Messages/Sources.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `if` condition in `FullSourcePreview` has incorrect operator precedence. The `&&` operator has higher precedence than `||`, so `!data?.url && !data?.isImage` is evaluated first, which could lead to unexpected behavior.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
  if (!data || (!data?.url && !data?.isImage)) return null;
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:47:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,6 +338,62 @@ export function truncateMiddle(str: string, charLimit: number) {
   return str.slice(0, frontChars) + ellipsis + str.slice(-backChars);
 }
 
+export function isGoogleDocsEmbeddable(input: string): boolean {
+  const supportedMimeTypes = new Set([
+    "application/pdf",
+    "application/msword",
+    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+    "application/vnd.ms-powerpoint",
+    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+    "application/vnd.ms-excel",
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    "text/plain",
+    "application/rtf",
+    "text/csv",
+  ]);
+
+  const supportedExtensions = new Set([
+    ".pdf",
+    ".doc",
+    ".docx",
+    ".ppt",
+    ".pptx",
+    ".xls",
+    ".xlsx",
+    ".txt",
+    ".rtf",
+  ]);
path: src/widgets/copilot/components/Messages/helpers.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `isGoogleDocsEmbeddable` function doesn't check for `.csv` extension in the URL validation loop despite it being included in the supported MIME types set.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
  const supportedExtensions = new Set([
    ".pdf",
    ".doc",
    ".docx",
    ".ppt",
    ".pptx",
    ".xls",
    ".xlsx",
    ".txt",
    ".rtf",
    ".csv",
  ]);
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,6 +338,62 @@ export function truncateMiddle(str: string, charLimit: number) {
   return str.slice(0, frontChars) + ellipsis + str.slice(-backChars);
 }
 
+export function isGoogleDocsEmbeddable(input: string): boolean {
+  const supportedMimeTypes = new Set([
+    "application/pdf",
+    "application/msword",
+    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+    "application/vnd.ms-powerpoint",
+    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+    "application/vnd.ms-excel",
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    "text/plain",
+    "application/rtf",
+    "text/csv",
+  ]);
+
+  const supportedExtensions = new Set([
+    ".pdf",
+    ".doc",
+    ".docx",
+    ".ppt",
+    ".pptx",
+    ".xls",
+    ".xlsx",
+    ".txt",
+    ".rtf",
+  ]);
+
+  // MIME type
+  if (input.includes("/") && !input.startsWith("http")) {
+    return supportedMimeTypes.has(input);
+  }
+
+  try {
+    const url = new URL(input);
+    let pathname = url.pathname.toLowerCase();
+
+    // Remove trailing slash if present
+    if (pathname.endsWith("/")) {
+      pathname = pathname.slice(0, -1);
+    }
+
+    // Extract filename from path
+    const filename = pathname.split("/").pop() || "";
+
+    // Check if the filename ends with any of the supported extensions
+    let match = false;
+    for (const ext of supportedExtensions) {
+      if (filename.endsWith(ext)) {
+        match = true;
+        break;
+      }
+    }
path: src/widgets/copilot/components/Messages/helpers.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `isGoogleDocsEmbeddable` function doesn't properly handle URLs with query parameters, as it checks if the filename ends with an extension but doesn't strip query parameters first.

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
    // Extract filename from path
    let filename = pathname.split("/").pop() || "";
    
    // Remove query parameters if present
    if (filename.includes("?")) {
      filename = filename.split("?")[0];
    }

    // Check if the filename ends with any of the supported extensions
    let match = false;
    for (const ext of supportedExtensions) {
      if (filename.endsWith(ext)) {
        match = true;
        break;
      }
    }
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:48:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +56,7 @@ const Messages = () => {
 
   useEffect(() => {
     // avoid autoplay on mount
-    avoidAutoplay();
+    if (avoidAutoplay) avoidAutoplay();
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
path: src/widgets/copilot/components/Messages/index.tsx
user: login: entelligence-ai-pr-reviews[bot]
body: **Correctness**: The `avoidAutoplay` function is conditionally called but not included in the dependency array of `useEffect`, which could lead to stale closures if `avoidAutoplay` changes between renders.

<!-- static_analysis_start -->
<details>
<summary><strong>⚠️ Flagged by Biome</strong></summary>

```
This hook does not specify all of its dependencies: avoidAutoplay
```
</details>
<!-- static_analysis_end -->

<!-- suggestion_start -->
<details>
<summary><strong>📝 Committable Code Suggestion</strong></summary>

> ‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

```suggestion
  useEffect(() => {
    // avoid autoplay on mount
    if (avoidAutoplay) avoidAutoplay();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [avoidAutoplay]);
```
</details>
<!-- suggestion_end -->

created_at: 2025-05-21T11:48:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-21T11:37:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces several new features and refactors across multiple components. New SVG icon components (`IconFile`, `IconImage`, `IconPlus`) are added and integrated into the button and file preview UI. The `Button` and `IconButton` components now support a "pressed" state and left icon rendering, with corresponding CSS changes. The file upload and preview functionality in the ChatInput component is expanded to support multiple file types, removable files, and sidebar previews, with modularized preview item components and improved UX. The Popper component is refactored to use Popper.js for positioning. The `CircularLoader` loader is simplified and now uses new CSS-based animation. Additional utility functions for file handling and embedding are introduced, and outgoing message rendering is updated to support attached files. Several CSS enhancements and minor internal logic changes are also included.

## Suggested reviewers

- devxpy

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> npm warn config production Use `--omit=dev` instead.
> npm error Exit handler never called!
> npm error This is an error with npm itself. Please report this error at:
> npm error   <https://github.com/npm/cli/issues>
> npm error A complete log of this run can be found in: /.npm/_logs/2025-05-21T12_11_15_761Z-debug-0.log
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=GooeyAI/gooey-web-widget&utm_content=20):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: entelligence-ai-pr-reviews[bot]
created_at: 2025-05-21T11:46:01Z
body: ## Review Summary

Skipped posting 5 drafted comments based on your review threshold. Feel free to update them [here](https://www.entelligence.ai/pull-requests).

<details>
<summary>Draft Comments</summary>

<details>
<summary>src/assets/SvgIcons/IconPlus.tsx:3-4</summary>

```
The component uses `any` type for props which reduces type safety. Using a proper interface or type definition would provide better type checking.
```

Scores:
- Production Impact: 2
- Fix Specificity: 5
- Urgency Impact: 3
- Total Score: 10

Reason for filtering: The comment addresses a valid type safety issue and provides a specific fix

Analysis: The issue reduces type safety which could lead to subtle bugs, but is unlikely to cause immediate production failures. The fix is very specific and directly applicable. While not urgent, addressing type safety issues is important for code maintainability and preventing future bugs. The total score meets the minimum threshold of 10.
</details>

<details>
<summary>src/contexts/MessagesContext.tsx:72-72</summary>

```
Using `delete` operator on `payload.attached_files` can impact performance. Using assignment to `undefined` is more efficient.
```

Scores:
- Production Impact: 2
- Fix Specificity: 5
- Urgency Impact: 3
- Total Score: 10

Reason for filtering: The comment meets the threshold for inclusion

Analysis: The performance impact of using delete vs. assignment to undefined is real but likely minor in most contexts. The fix is extremely clear and directly applicable with a commitable suggestion. While not critical, optimizing performance is moderately important for maintaining good user experience. The total score meets the minimum threshold of 10 for inclusion.
</details>

<details>
<summary>src/contexts/SystemContext.tsx:174-174</summary>

```
The `toggleSecondaryDrawer` function contains several ternary expressions that could be simplified for better readability and maintainability, as flagged by the linter.
```

Scores:
- Production Impact: 1
- Fix Specificity: 5
- Urgency Impact: 2
- Total Score: 8

Reason for filtering: The comment meets the scoring threshold and provides a valid style improvement

Analysis: This is a style improvement that simplifies a boolean expression. The fix is very specific and directly applicable (score 5). It has minimal production impact (score 1) as it's purely a code style issue. The urgency is low (score 2) as it's a linter-flagged style issue that doesn't affect functionality. With a total score of 8, this comment falls below the threshold of 10 required for inclusion under the current filtering rules.
</details>

<details>
<summary>src/widgets/copilot/components/ChatInput/FilePreview.tsx:85-85</summary>

```
Non-self-closing div element should be self-closing for better JSX consistency when it has no children.
```

Scores:
- Production Impact: 1
- Fix Specificity: 5
- Urgency Impact: 1
- Total Score: 7

Reason for filtering: The comment is valid and provides a clear fix for a style issue

Analysis: This is a purely stylistic JSX issue with no functional impact. The fix is extremely clear and directly applicable. While the fix improves code consistency, it has no urgency as it doesn't affect functionality. With a total score of 7, this comment falls below the threshold of 10 and should be removed under current filtering rules.
</details>

<details>
<summary>src/widgets/copilot/components/Messages/helpers.tsx:420-451</summary>

```
The `renderImageInIframe` function creates a blob URL but doesn't handle cleanup, which can lead to memory leaks as these URLs need to be explicitly revoked when no longer needed.
```

Scores:
- Production Impact: 3
- Fix Specificity: 4
- Urgency Impact: 3
- Total Score: 10

Reason for filtering: The comment identifies a legitimate memory leak issue with proper context

Analysis: The bug describes a memory leak from not revoking blob URLs, which is a real issue. While not causing immediate crashes, it can degrade performance over time. The fix is specific (implement URL.revokeObjectURL when done) and straightforward. The issue should be addressed in the near term but won't cause immediate system failure.
</details>

</details>


author_association: CONTRIBUTOR