Browse Source

refactor(frontend): App index route (mainly file explorer) (#5287)

sp.wack 1 year ago
parent
commit
5d366129d1

+ 1 - 1
frontend/__tests__/components/file-explorer/file-explorer.test.tsx

@@ -4,7 +4,7 @@ import { renderWithProviders } from "test-utils";
 import { describe, it, expect, vi, Mock, afterEach } from "vitest";
 import toast from "#/utils/toast";
 import AgentState from "#/types/agent-state";
-import FileExplorer from "#/components/file-explorer/file-explorer";
+import { FileExplorer } from "#/routes/_oh.app._index/file-explorer/file-explorer";
 import OpenHands from "#/api/open-hands";
 
 const toastSpy = vi.spyOn(toast, "error");

+ 0 - 307
frontend/src/components/file-explorer/file-explorer.tsx

@@ -1,307 +0,0 @@
-import React from "react";
-import {
-  IoIosArrowBack,
-  IoIosArrowForward,
-  IoIosRefresh,
-  IoIosCloudUpload,
-} from "react-icons/io";
-import { useDispatch, useSelector } from "react-redux";
-import { IoFileTray } from "react-icons/io5";
-import { useTranslation } from "react-i18next";
-import { twMerge } from "tailwind-merge";
-import AgentState from "#/types/agent-state";
-import { addAssistantMessage } from "#/state/chat-slice";
-import IconButton from "../icon-button";
-import ExplorerTree from "./explorer-tree";
-import toast from "#/utils/toast";
-import { RootState } from "#/store";
-import { I18nKey } from "#/i18n/declaration";
-import OpenHands from "#/api/open-hands";
-import VSCodeIcon from "#/assets/vscode-alt.svg?react";
-import { useListFiles } from "#/hooks/query/use-list-files";
-import { FileUploadSuccessResponse } from "#/api/open-hands.types";
-import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
-
-interface ExplorerActionsProps {
-  onRefresh: () => void;
-  onUpload: () => void;
-  toggleHidden: () => void;
-  isHidden: boolean;
-}
-
-function ExplorerActions({
-  toggleHidden,
-  onRefresh,
-  onUpload,
-  isHidden,
-}: ExplorerActionsProps) {
-  return (
-    <div
-      className={twMerge(
-        "transform flex h-[24px] items-center gap-1",
-        isHidden ? "right-3" : "right-2",
-      )}
-    >
-      {!isHidden && (
-        <>
-          <IconButton
-            icon={
-              <IoIosRefresh
-                size={16}
-                className="text-neutral-400 hover:text-neutral-100 transition"
-              />
-            }
-            testId="refresh"
-            ariaLabel="Refresh workspace"
-            onClick={onRefresh}
-          />
-          <IconButton
-            icon={
-              <IoIosCloudUpload
-                size={16}
-                className="text-neutral-400 hover:text-neutral-100 transition"
-              />
-            }
-            testId="upload"
-            ariaLabel="Upload File"
-            onClick={onUpload}
-          />
-        </>
-      )}
-
-      <IconButton
-        icon={
-          isHidden ? (
-            <IoIosArrowForward
-              size={20}
-              className="text-neutral-400 hover:text-neutral-100 transition"
-            />
-          ) : (
-            <IoIosArrowBack
-              size={20}
-              className="text-neutral-400 hover:text-neutral-100 transition"
-            />
-          )
-        }
-        testId="toggle"
-        ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
-        onClick={toggleHidden}
-      />
-    </div>
-  );
-}
-
-interface FileExplorerProps {
-  isOpen: boolean;
-  onToggle: () => void;
-}
-
-function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
-  const [isDragging, setIsDragging] = React.useState(false);
-
-  const { curAgentState } = useSelector((state: RootState) => state.agent);
-  const fileInputRef = React.useRef<HTMLInputElement | null>(null);
-  const dispatch = useDispatch();
-  const { t } = useTranslation();
-  const selectFileInput = () => {
-    fileInputRef.current?.click(); // Trigger the file browser
-  };
-
-  const { data: paths, refetch, error } = useListFiles();
-
-  const handleUploadSuccess = (data: FileUploadSuccessResponse) => {
-    const uploadedCount = data.uploaded_files.length;
-    const skippedCount = data.skipped_files.length;
-
-    if (uploadedCount > 0) {
-      toast.success(
-        `upload-success-${new Date().getTime()}`,
-        t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, {
-          count: uploadedCount,
-        }),
-      );
-    }
-
-    if (skippedCount > 0) {
-      const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, {
-        count: skippedCount,
-      });
-      toast.info(message);
-    }
-
-    if (uploadedCount === 0 && skippedCount === 0) {
-      toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE));
-    }
-  };
-
-  const handleUploadError = (e: Error) => {
-    toast.error(
-      `upload-error-${new Date().getTime()}`,
-      e.message || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
-    );
-  };
-
-  const { mutate: uploadFiles } = useUploadFiles();
-
-  const refreshWorkspace = () => {
-    if (
-      curAgentState !== AgentState.LOADING &&
-      curAgentState !== AgentState.STOPPED
-    ) {
-      refetch();
-    }
-  };
-
-  const uploadFileData = (files: FileList) => {
-    uploadFiles(
-      { files: Array.from(files) },
-      { onSuccess: handleUploadSuccess, onError: handleUploadError },
-    );
-    refreshWorkspace();
-  };
-
-  const handleVSCodeClick = async (e: React.MouseEvent) => {
-    e.preventDefault();
-    try {
-      const response = await OpenHands.getVSCodeUrl();
-      if (response.vscode_url) {
-        dispatch(
-          addAssistantMessage(
-            "You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
-          ),
-        );
-        window.open(response.vscode_url, "_blank");
-      } else {
-        toast.error(
-          `open-vscode-error-${new Date().getTime()}`,
-          t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
-            error: response.error,
-          }),
-        );
-      }
-    } catch (exp_error) {
-      toast.error(
-        `open-vscode-error-${new Date().getTime()}`,
-        t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
-          error: String(exp_error),
-        }),
-      );
-    }
-  };
-
-  React.useEffect(() => {
-    refreshWorkspace();
-  }, [curAgentState]);
-
-  return (
-    <div
-      data-testid="file-explorer"
-      className="relative h-full"
-      onDragEnter={() => {
-        setIsDragging(true);
-      }}
-      onDragEnd={() => {
-        setIsDragging(false);
-      }}
-    >
-      {isDragging && (
-        <div
-          data-testid="dropzone"
-          onDragLeave={() => setIsDragging(false)}
-          onDrop={(event) => {
-            event.preventDefault();
-            const { files: droppedFiles } = event.dataTransfer;
-            if (droppedFiles.length > 0) {
-              uploadFileData(droppedFiles);
-            }
-            setIsDragging(false);
-          }}
-          onDragOver={(event) => event.preventDefault()}
-          className="z-10 absolute flex flex-col justify-center items-center bg-black top-0 bottom-0 left-0 right-0 opacity-65"
-        >
-          <IoFileTray size={32} />
-          <p className="font-bold text-xl">
-            {t(I18nKey.EXPLORER$LABEL_DROP_FILES)}
-          </p>
-        </div>
-      )}
-      <div
-        className={twMerge(
-          "bg-neutral-800 h-full border-r-1 border-r-neutral-600 flex flex-col",
-          !isOpen ? "w-12" : "w-60",
-        )}
-      >
-        <div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
-          <div className="sticky top-0 bg-neutral-800">
-            <div
-              className={twMerge(
-                "flex items-center",
-                !isOpen ? "justify-center" : "justify-between",
-              )}
-            >
-              {isOpen && (
-                <div className="text-neutral-300 font-bold text-sm">
-                  {t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
-                </div>
-              )}
-              <ExplorerActions
-                isHidden={!isOpen}
-                toggleHidden={onToggle}
-                onRefresh={refreshWorkspace}
-                onUpload={selectFileInput}
-              />
-            </div>
-          </div>
-          {!error && (
-            <div className="overflow-auto flex-grow min-h-0">
-              <div style={{ display: !isOpen ? "none" : "block" }}>
-                <ExplorerTree files={paths || []} />
-              </div>
-            </div>
-          )}
-          {error && (
-            <div className="flex flex-col items-center justify-center h-full">
-              <p className="text-neutral-300 text-sm">{error.message}</p>
-            </div>
-          )}
-          {isOpen && (
-            <button
-              type="button"
-              onClick={handleVSCodeClick}
-              disabled={
-                curAgentState === AgentState.INIT ||
-                curAgentState === AgentState.LOADING
-              }
-              className={twMerge(
-                "mt-auto mb-2 w-full h-10 text-white rounded flex items-center justify-center gap-2 transition-colors",
-                curAgentState === AgentState.INIT ||
-                  curAgentState === AgentState.LOADING
-                  ? "bg-neutral-600 cursor-not-allowed"
-                  : "bg-[#4465DB] hover:bg-[#3451C7]",
-              )}
-              aria-label="Open in VS Code"
-            >
-              <VSCodeIcon width={20} height={20} />
-              Open in VS Code
-            </button>
-          )}
-        </div>
-        <input
-          data-testid="file-input"
-          type="file"
-          multiple
-          ref={fileInputRef}
-          style={{ display: "none" }}
-          onChange={(event) => {
-            const { files: selectedFiles } = event.target;
-            if (selectedFiles && selectedFiles.length > 0) {
-              uploadFileData(selectedFiles);
-            }
-          }}
-        />
-      </div>
-    </div>
-  );
-}
-
-export default FileExplorer;

+ 43 - 0
frontend/src/hooks/query/use-vscode-url.ts

@@ -0,0 +1,43 @@
+import { useQuery } from "@tanstack/react-query";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { useDispatch } from "react-redux";
+import toast from "#/utils/toast";
+import { addAssistantMessage } from "#/state/chat-slice";
+import { I18nKey } from "#/i18n/declaration";
+import OpenHands from "#/api/open-hands";
+
+export const useVSCodeUrl = () => {
+  const { t } = useTranslation();
+  const dispatch = useDispatch();
+
+  const data = useQuery({
+    queryKey: ["vscode_url"],
+    queryFn: OpenHands.getVSCodeUrl,
+    enabled: false,
+  });
+
+  const { data: vscodeUrlObject, isFetching } = data;
+
+  React.useEffect(() => {
+    if (isFetching) return;
+
+    if (vscodeUrlObject?.vscode_url) {
+      dispatch(
+        addAssistantMessage(
+          "You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
+        ),
+      );
+      window.open(vscodeUrlObject.vscode_url, "_blank");
+    } else if (vscodeUrlObject?.error) {
+      toast.error(
+        `open-vscode-error-${new Date().getTime()}`,
+        t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
+          error: vscodeUrlObject.error,
+        }),
+      );
+    }
+  }, [vscodeUrlObject, isFetching]);
+
+  return data;
+};

+ 11 - 0
frontend/src/routes/_oh.app._index/constants.ts

@@ -0,0 +1,11 @@
+export const ASSET_FILE_TYPES = [
+  ".png",
+  ".jpg",
+  ".jpeg",
+  ".bmp",
+  ".gif",
+  ".pdf",
+  ".mp4",
+  ".webm",
+  ".ogg",
+];

+ 30 - 0
frontend/src/routes/_oh.app._index/file-explorer/buttons/open-vscode-button.tsx

@@ -0,0 +1,30 @@
+import { cn } from "#/utils/utils";
+import VSCodeIcon from "#/assets/vscode-alt.svg?react";
+
+interface OpenVSCodeButtonProps {
+  isDisabled: boolean;
+  onClick: () => void;
+}
+
+export function OpenVSCodeButton({
+  isDisabled,
+  onClick,
+}: OpenVSCodeButtonProps) {
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      disabled={isDisabled}
+      className={cn(
+        "mt-auto mb-2 w-full h-10 text-white rounded flex items-center justify-center gap-2 transition-colors",
+        isDisabled
+          ? "bg-neutral-600 cursor-not-allowed"
+          : "bg-[#4465DB] hover:bg-[#3451C7]",
+      )}
+      aria-label="Open in VS Code"
+    >
+      <VSCodeIcon width={20} height={20} />
+      Open in VS Code
+    </button>
+  );
+}

+ 22 - 0
frontend/src/routes/_oh.app._index/file-explorer/buttons/refresh-icon-button.tsx

@@ -0,0 +1,22 @@
+import { IoIosRefresh } from "react-icons/io";
+import IconButton from "#/components/icon-button";
+
+interface RefreshIconButtonProps {
+  onClick: () => void;
+}
+
+export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
+  return (
+    <IconButton
+      icon={
+        <IoIosRefresh
+          size={16}
+          className="text-neutral-400 hover:text-neutral-100 transition"
+        />
+      }
+      testId="refresh"
+      ariaLabel="Refresh workspace"
+      onClick={onClick}
+    />
+  );
+}

+ 33 - 0
frontend/src/routes/_oh.app._index/file-explorer/buttons/toggle-workspace-icon-button.tsx

@@ -0,0 +1,33 @@
+import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";
+import IconButton from "#/components/icon-button";
+
+interface ToggleWorkspaceIconButtonProps {
+  onClick: () => void;
+  isHidden: boolean;
+}
+
+export function ToggleWorkspaceIconButton({
+  onClick,
+  isHidden,
+}: ToggleWorkspaceIconButtonProps) {
+  return (
+    <IconButton
+      icon={
+        isHidden ? (
+          <IoIosArrowForward
+            size={20}
+            className="text-neutral-400 hover:text-neutral-100 transition"
+          />
+        ) : (
+          <IoIosArrowBack
+            size={20}
+            className="text-neutral-400 hover:text-neutral-100 transition"
+          />
+        )
+      }
+      testId="toggle"
+      ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
+      onClick={onClick}
+    />
+  );
+}

+ 22 - 0
frontend/src/routes/_oh.app._index/file-explorer/buttons/upload-icon-button.tsx

@@ -0,0 +1,22 @@
+import { IoIosCloudUpload } from "react-icons/io";
+import IconButton from "#/components/icon-button";
+
+interface UploadIconButtonProps {
+  onClick: () => void;
+}
+
+export function UploadIconButton({ onClick }: UploadIconButtonProps) {
+  return (
+    <IconButton
+      icon={
+        <IoIosCloudUpload
+          size={16}
+          className="text-neutral-400 hover:text-neutral-100 transition"
+        />
+      }
+      testId="upload"
+      ariaLabel="Upload File"
+      onClick={onClick}
+    />
+  );
+}

+ 27 - 0
frontend/src/routes/_oh.app._index/file-explorer/dropzone.tsx

@@ -0,0 +1,27 @@
+import { useTranslation } from "react-i18next";
+import { IoFileTray } from "react-icons/io5";
+import { I18nKey } from "#/i18n/declaration";
+
+interface DropzoneProps {
+  onDragLeave: () => void;
+  onDrop: (event: React.DragEvent<HTMLDivElement>) => void;
+}
+
+export function Dropzone({ onDragLeave, onDrop }: DropzoneProps) {
+  const { t } = useTranslation();
+
+  return (
+    <div
+      data-testid="dropzone"
+      onDragLeave={onDragLeave}
+      onDrop={onDrop}
+      onDragOver={(event) => event.preventDefault()}
+      className="z-10 absolute flex flex-col justify-center items-center bg-black top-0 bottom-0 left-0 right-0 opacity-65"
+    >
+      <IoFileTray size={32} />
+      <p className="font-bold text-xl">
+        {t(I18nKey.EXPLORER$LABEL_DROP_FILES)}
+      </p>
+    </div>
+  );
+}

+ 36 - 0
frontend/src/routes/_oh.app._index/file-explorer/file-explorer-actions.tsx

@@ -0,0 +1,36 @@
+import { cn } from "#/utils/utils";
+import { RefreshIconButton } from "./buttons/refresh-icon-button";
+import { ToggleWorkspaceIconButton } from "./buttons/toggle-workspace-icon-button";
+import { UploadIconButton } from "./buttons/upload-icon-button";
+
+interface ExplorerActionsProps {
+  onRefresh: () => void;
+  onUpload: () => void;
+  toggleHidden: () => void;
+  isHidden: boolean;
+}
+
+export function ExplorerActions({
+  toggleHidden,
+  onRefresh,
+  onUpload,
+  isHidden,
+}: ExplorerActionsProps) {
+  return (
+    <div
+      className={cn(
+        "flex h-[24px] items-center gap-1",
+        isHidden ? "right-3" : "right-2",
+      )}
+    >
+      {!isHidden && (
+        <>
+          <RefreshIconButton onClick={onRefresh} />
+          <UploadIconButton onClick={onUpload} />
+        </>
+      )}
+
+      <ToggleWorkspaceIconButton isHidden={isHidden} onClick={toggleHidden} />
+    </div>
+  );
+}

+ 42 - 0
frontend/src/routes/_oh.app._index/file-explorer/file-explorer-header.tsx

@@ -0,0 +1,42 @@
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
+import { cn } from "#/utils/utils";
+import { ExplorerActions } from "./file-explorer-actions";
+
+interface FileExplorerHeaderProps {
+  isOpen: boolean;
+  onToggle: () => void;
+  onRefreshWorkspace: () => void;
+  onUploadFile: () => void;
+}
+
+export function FileExplorerHeader({
+  isOpen,
+  onToggle,
+  onRefreshWorkspace,
+  onUploadFile,
+}: FileExplorerHeaderProps) {
+  const { t } = useTranslation();
+
+  return (
+    <div
+      className={cn(
+        "sticky top-0 bg-neutral-800",
+        "flex items-center",
+        !isOpen ? "justify-center" : "justify-between",
+      )}
+    >
+      {isOpen && (
+        <div className="text-neutral-300 font-bold text-sm">
+          {t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
+        </div>
+      )}
+      <ExplorerActions
+        isHidden={!isOpen}
+        toggleHidden={onToggle}
+        onRefresh={onRefreshWorkspace}
+        onUpload={onUploadFile}
+      />
+    </div>
+  );
+}

+ 156 - 0
frontend/src/routes/_oh.app._index/file-explorer/file-explorer.tsx

@@ -0,0 +1,156 @@
+import React from "react";
+import { useSelector } from "react-redux";
+import { useTranslation } from "react-i18next";
+import AgentState from "#/types/agent-state";
+import ExplorerTree from "../../../components/file-explorer/explorer-tree";
+import toast from "#/utils/toast";
+import { RootState } from "#/store";
+import { I18nKey } from "#/i18n/declaration";
+import { useListFiles } from "#/hooks/query/use-list-files";
+import { FileUploadSuccessResponse } from "#/api/open-hands.types";
+import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
+import { cn } from "#/utils/utils";
+import { OpenVSCodeButton } from "./buttons/open-vscode-button";
+import { Dropzone } from "./dropzone";
+import { FileExplorerHeader } from "./file-explorer-header";
+import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
+
+interface FileExplorerProps {
+  isOpen: boolean;
+  onToggle: () => void;
+}
+
+export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
+  const { t } = useTranslation();
+
+  const fileInputRef = React.useRef<HTMLInputElement | null>(null);
+  const [isDragging, setIsDragging] = React.useState(false);
+
+  const { curAgentState } = useSelector((state: RootState) => state.agent);
+
+  const { data: paths, refetch, error } = useListFiles();
+  const { mutate: uploadFiles } = useUploadFiles();
+  const { refetch: getVSCodeUrl } = useVSCodeUrl();
+
+  const selectFileInput = () => {
+    fileInputRef.current?.click(); // Trigger the file browser
+  };
+
+  const handleUploadSuccess = (data: FileUploadSuccessResponse) => {
+    const uploadedCount = data.uploaded_files.length;
+    const skippedCount = data.skipped_files.length;
+
+    if (uploadedCount > 0) {
+      toast.success(
+        `upload-success-${new Date().getTime()}`,
+        t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, {
+          count: uploadedCount,
+        }),
+      );
+    }
+
+    if (skippedCount > 0) {
+      const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, {
+        count: skippedCount,
+      });
+      toast.info(message);
+    }
+
+    if (uploadedCount === 0 && skippedCount === 0) {
+      toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE));
+    }
+  };
+
+  const handleUploadError = (uploadError: Error) => {
+    toast.error(
+      `upload-error-${new Date().getTime()}`,
+      uploadError.message || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
+    );
+  };
+
+  const refreshWorkspace = () => {
+    if (
+      curAgentState !== AgentState.LOADING &&
+      curAgentState !== AgentState.STOPPED
+    ) {
+      refetch();
+    }
+  };
+
+  const uploadFileData = (files: FileList) => {
+    uploadFiles(
+      { files: Array.from(files) },
+      { onSuccess: handleUploadSuccess, onError: handleUploadError },
+    );
+    refreshWorkspace();
+  };
+
+  const handleDropFiles = (event: React.DragEvent<HTMLDivElement>) => {
+    event.preventDefault();
+    const { files: droppedFiles } = event.dataTransfer;
+    if (droppedFiles.length > 0) {
+      uploadFileData(droppedFiles);
+    }
+    setIsDragging(false);
+  };
+
+  React.useEffect(() => {
+    refreshWorkspace();
+  }, [curAgentState]);
+
+  return (
+    <div
+      data-testid="file-explorer"
+      className="relative h-full"
+      onDragEnter={() => {
+        setIsDragging(true);
+      }}
+      onDragEnd={() => {
+        setIsDragging(false);
+      }}
+    >
+      {isDragging && (
+        <Dropzone
+          onDragLeave={() => setIsDragging(false)}
+          onDrop={handleDropFiles}
+        />
+      )}
+      <div
+        className={cn(
+          "bg-neutral-800 h-full border-r-1 border-r-neutral-600 flex flex-col",
+          !isOpen ? "w-12" : "w-60",
+        )}
+      >
+        <div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
+          <FileExplorerHeader
+            isOpen={isOpen}
+            onToggle={onToggle}
+            onRefreshWorkspace={refreshWorkspace}
+            onUploadFile={selectFileInput}
+          />
+          {!error && (
+            <div className="overflow-auto flex-grow min-h-0">
+              <div style={{ display: !isOpen ? "none" : "block" }}>
+                <ExplorerTree files={paths || []} />
+              </div>
+            </div>
+          )}
+          {error && (
+            <div className="flex flex-col items-center justify-center h-full">
+              <p className="text-neutral-300 text-sm">{error.message}</p>
+            </div>
+          )}
+          {isOpen && (
+            <OpenVSCodeButton
+              onClick={getVSCodeUrl}
+              isDisabled={
+                curAgentState === AgentState.INIT ||
+                curAgentState === AgentState.LOADING
+              }
+            />
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 2 - 13
frontend/src/routes/_oh.app._index/route.tsx

@@ -5,23 +5,12 @@ import { editor } from "monaco-editor";
 import { EditorProps } from "@monaco-editor/react";
 import { RootState } from "#/store";
 import AgentState from "#/types/agent-state";
-import FileExplorer from "#/components/file-explorer/file-explorer";
+import { FileExplorer } from "#/routes/_oh.app._index/file-explorer/file-explorer";
 import CodeEditorComponent from "./code-editor-component";
 import { useFiles } from "#/context/files";
 import { EditorActions } from "#/components/editor-actions";
 import { useSaveFile } from "#/hooks/mutation/use-save-file";
-
-const ASSET_FILE_TYPES = [
-  ".png",
-  ".jpg",
-  ".jpeg",
-  ".bmp",
-  ".gif",
-  ".pdf",
-  ".mp4",
-  ".webm",
-  ".ogg",
-];
+import { ASSET_FILE_TYPES } from "./constants";
 
 export function ErrorBoundary() {
   const error = useRouteError();