| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- 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/AgentState";
- import { setRefreshID } from "#/state/codeSlice";
- import { listFiles, uploadFiles } from "#/services/fileService";
- import IconButton from "../IconButton";
- import ExplorerTree from "./ExplorerTree";
- import toast from "#/utils/toast";
- import { RootState } from "#/store";
- import { I18nKey } from "#/i18n/declaration";
- 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>
- );
- }
- function FileExplorer() {
- const [isHidden, setIsHidden] = React.useState(false);
- const [isDragging, setIsDragging] = React.useState(false);
- const [files, setFiles] = React.useState<string[]>([]);
- 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 refreshWorkspace = async () => {
- if (
- curAgentState === AgentState.LOADING ||
- curAgentState === AgentState.STOPPED
- ) {
- return;
- }
- dispatch(setRefreshID(Math.random()));
- try {
- const fileList = await listFiles("/");
- setFiles(fileList);
- if (fileList.length === 0) {
- toast.info(t(I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE));
- }
- } catch (error) {
- toast.error("refresh-error", t(I18nKey.EXPLORER$REFRESH_ERROR_MESSAGE));
- }
- };
- const uploadFileData = async (toAdd: FileList) => {
- try {
- const result = await uploadFiles(toAdd);
- if (result.error) {
- // Handle error response
- toast.error(
- `upload-error-${new Date().getTime()}`,
- result.error || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
- );
- return;
- }
- const uploadedCount = result.uploadedFiles.length;
- const skippedCount = result.skippedFiles.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));
- }
- await refreshWorkspace();
- } catch (error) {
- // Handle unexpected errors (network issues, etc.)
- toast.error(
- `upload-error-${new Date().getTime()}`,
- t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
- );
- }
- };
- React.useEffect(() => {
- (async () => {
- await refreshWorkspace();
- })();
- }, [curAgentState]);
- React.useEffect(() => {
- const enableDragging = () => {
- setIsDragging(true);
- };
- const disableDragging = () => {
- setIsDragging(false);
- };
- document.addEventListener("dragenter", enableDragging);
- document.addEventListener("drop", disableDragging);
- return () => {
- document.removeEventListener("dragenter", enableDragging);
- document.removeEventListener("drop", disableDragging);
- };
- }, []);
- if (!files.length) {
- return null;
- }
- return (
- <div className="relative h-full">
- {isDragging && (
- <div
- data-testid="dropzone"
- onDrop={(event) => {
- event.preventDefault();
- const { files: droppedFiles } = event.dataTransfer;
- if (droppedFiles.length > 0) {
- uploadFileData(droppedFiles);
- }
- }}
- 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 transition-all ease-soft-spring",
- isHidden ? "min-w-[48px]" : "min-w-[228px]",
- )}
- >
- <div className="flex flex-col relative h-full">
- <div className="sticky top-0 bg-neutral-800 z-10">
- <div
- className={twMerge(
- "flex items-center mt-2 mb-1 p-2",
- isHidden ? "justify-center" : "justify-between",
- )}
- >
- {!isHidden && (
- <div className="ml-1 text-neutral-300 font-bold text-sm">
- <div className="ml-1 text-neutral-300 font-bold text-sm">
- {t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
- </div>
- </div>
- )}
- <ExplorerActions
- isHidden={isHidden}
- toggleHidden={() => setIsHidden((prev) => !prev)}
- onRefresh={refreshWorkspace}
- onUpload={selectFileInput}
- />
- </div>
- </div>
- <div className="overflow-auto flex-grow">
- <div style={{ display: isHidden ? "none" : "block" }}>
- <ExplorerTree files={files} defaultOpen />
- </div>
- </div>
- </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;
|