Repository: gradio-app/gradio
Pull request: Improve File Explorer performance
Previously we would load the entire directory structure at the root directory and send that to the frontend when the fileexplorer component loaded. This could be prohibitively expensive for higher level directories to crawl through all the nested subdirectories, taking on the order of minutes to load. Now the API call only returns the content of a single level, and when opening a folder, we load the content of that folder. See gif below, showing network requests as well.

![Recording 2024-02-06 at 21 08 25](https://github.com/gradio-app/gradio/assets/7870876/54db2a1d-2d94-421c-9720-f319229c66c0)

Also fixed root_dir update issue.
Fixes: #7149 

Demo code to repro fix (change root_dir values ofc):
```python
import gradio as gr

with gr.Blocks() as app:
    file_explorer = gr.FileExplorer(
        value=["/Users/aliabid/Desktop/gradio/demo/hello_blocks/run.py", "/Users/aliabid/Desktop/gradio/demo/change_vs_input/run.py"],
        root_dir="/Users/aliabid/Desktop/gradio/demo/", 
        file_count="multiple",
        height=200
    )
    select_btn = gr.Button("Select")
    selected_file = gr.Textbox(label="Selected")

    select_btn.click(lambda x:x, inputs=file_explorer, outputs=selected_file)

    change_root_btn = gr.Button("Change Root")
    change_root_btn.click(lambda: gr.FileExplorer(root_dir="/Users/aliabid/Desktop/gradio/"), outputs=file_explorer)


if __name__ == "__main__":
    app.launch()
```

Problem statement:
Updating the root parameter in the fileexplorer component does not change the front-end display of the file directory.
### Describe the bug

The gr.fileexplorer() object changes the root parameter when used as a callback function, but it does not actually change the root directory of the component for file searches.
I want to know how to change the root directory of gr.fileexplorer()  

### Have you searched existing issues?  🔎

- [X] I have searched and found no existing issues

### Reproduction

```python
import gradio as gr

with gr.Blocks() as app:
    objFileExplorer = gr.FileExplorer(root="/home/file_path1/", show_label=True, height=400, interactive=True)
    objButtonClear = gr.Button("clear")

    @objButtonClear.click(inputs=[objFileExplorer], outputs=[objFileExplorer])
    def fnButtonClearClick(listSelectedFiles):
        print(listSelectedFiles)
        return gr.update(root="/home/file_path2/", value="", show_label=True, height=600, interactive=False)

if __name__ == "__main__":
    app.queue().launch(server_name='0.0.0.0',
                       server_port=40123,
                       share=False,
                       debug=True,
                       max_threads=2,
                       )

```


### Screenshot

_No response_

### Logs

_No response_

### System Info

```shell
gradio==4.4.0
```


### Severity

Blocking usage of gradio

Hints from issue discussion:
Hi @lifeisgoodcdj is this still an issue in the latest version of Gradio (4.15.0)? I believe we’ve fixed it 
The issue still exists. I have tried the 4.15.0 version of gradio, but I still cannot update or switch directories actually
cc @aliabid94 
i also need to update root dir ~ 
Hi @lifeisgoodcdj - the `root` parameter is deprecated for the FileExplorer. If you use `root_dir` in both the definition of `objFileExplorer` and `gr.update` it should work. If you want to use `root`, you can return a new instance of `gr.FileExplorer` in your update function. 

Going to close as this seems like a corner case with a deprecated API and fixing would require adding some component specific logic to the app-level postprocessing which would not be ideal for maintainability.

Commit message: changes

Patch to review:
diff --git a/.changeset/chatty-rules-press.md b/.changeset/chatty-rules-press.md
new file mode 100644
index 0000000000..dcec9ee9ec
--- /dev/null
+++ b/.changeset/chatty-rules-press.md
@@ -0,0 +1,6 @@
+---
+"@gradio/fileexplorer": patch
+"gradio": patch
+---
+
+fix:Improve File Explorer performance
diff --git a/demo/file_explorer_component_events/dir3/dir3_bar.log b/demo/file_explorer_component_events/dir3/dir3_bar.log
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log b/demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/demo/file_explorer_component_events/run.ipynb b/demo/file_explorer_component_events/run.ipynb
index 60efe8e1e6..830a8af342 100644
--- a/demo/file_explorer_component_events/run.ipynb
+++ b/demo/file_explorer_component_events/run.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer_component_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('dir1')\n", "!wget -q -O dir1/bar.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/bar.txt\n", "!wget -q -O dir1/foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/foo.txt\n", "os.mkdir('dir2')\n", "!wget -q -O dir2/baz.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/baz.png\n", "!wget -q -O dir2/foo.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/foo.png\n", "os.mkdir('dir3')\n", "!wget -q -O dir3/dir3_foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_foo.txt\n", "!wget -q -O dir3/dir4 https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "base_root = Path(__file__).parent.resolve()\n", "\n", "with gr.Blocks() as demo:\n", "    with gr.Row():\n", "        dd = gr.Dropdown(label=\"Select File Explorer Root\",\n", "                        value=str(base_root / \"dir1\"),\n", "                        choices=[str(base_root / \"dir1\"), str(base_root / \"dir2\"),\n", "                                 str(base_root / \"dir3\")])\n", "        with gr.Group():\n", "            dir_only_glob = gr.Checkbox(label=\"Show only directories\", value=False)\n", "            ignore_dir_in_glob = gr.Checkbox(label=\"Ignore directories in glob\", value=False)\n", "\n", "    fe = gr.FileExplorer(root=str(base_root / \"dir1\"),\n", "                         glob=\"**/*\", interactive=True)\n", "    textbox = gr.Textbox(label=\"Selected Directory\")\n", "    run = gr.Button(\"Run\")\n", "    \n", "    dir_only_glob.select(lambda s: gr.FileExplorer(glob=\"**/\" if s else \"**/*.*\",\n", "                                                   file_count=\"multiple\",\n", "                                                   root=str(base_root / \"dir3\")) ,\n", "                         inputs=[dir_only_glob], outputs=[fe])\n", "    ignore_dir_in_glob.select(lambda s: gr.FileExplorer(glob=\"**/*\",\n", "                                                        ignore_glob=\"**/\",\n", "                                                        root=str(base_root / \"dir3\")),\n", "                            inputs=[ignore_dir_in_glob], outputs=[fe])            \n", "\n", "    dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe])\n", "    run.click(lambda s: \",\".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])\n", "\n", "    with gr.Row():\n", "        a = gr.Textbox(elem_id=\"input-box\")\n", "        a.change(lambda x: x, inputs=[a])\n", "\n", "\n", "if __name__ == \"__main__\":\n", "    demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: file_explorer_component_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('dir1')\n", "!wget -q -O dir1/bar.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/bar.txt\n", "!wget -q -O dir1/foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir1/foo.txt\n", "os.mkdir('dir2')\n", "!wget -q -O dir2/baz.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/baz.png\n", "!wget -q -O dir2/foo.png https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir2/foo.png\n", "os.mkdir('dir3')\n", "!wget -q -O dir3/dir3_bar.log https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_bar.log\n", "!wget -q -O dir3/dir3_foo.txt https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir3_foo.txt\n", "!wget -q -O dir3/dir4 https://github.com/gradio-app/gradio/raw/main/demo/file_explorer_component_events/dir3/dir4"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "base_root = Path(__file__).parent.resolve()\n", "\n", "with gr.Blocks() as demo:\n", "    with gr.Row():\n", "        dd = gr.Dropdown(label=\"Select File Explorer Root\",\n", "                        value=str(base_root / \"dir1\"),\n", "                        choices=[str(base_root / \"dir1\"), str(base_root / \"dir2\"),\n", "                                 str(base_root / \"dir3\")])\n", "        with gr.Group():\n", "            txt_only_glob = gr.Checkbox(label=\"Show only text files\", value=False)\n", "            ignore_txt_in_glob = gr.Checkbox(label=\"Ignore text files in glob\", value=False)\n", "\n", "    fe = gr.FileExplorer(root=str(base_root / \"dir1\"),\n", "                         glob=\"**/*\", interactive=True)\n", "    textbox = gr.Textbox(label=\"Selected Directory\")\n", "    run = gr.Button(\"Run\")\n", "    \n", "    txt_only_glob.select(lambda s: gr.FileExplorer(glob=\"*.txt\" if s else \"*\") ,\n", "                         inputs=[txt_only_glob], outputs=[fe])\n", "    ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob=\"*.txt\" if s else None),\n", "                            inputs=[ignore_txt_in_glob], outputs=[fe])            \n", "\n", "    dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe])\n", "    run.click(lambda s: \",\".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])\n", "\n", "    with gr.Row():\n", "        a = gr.Textbox(elem_id=\"input-box\")\n", "        a.change(lambda x: x, inputs=[a])\n", "\n", "\n", "if __name__ == \"__main__\":\n", "    demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
diff --git a/demo/file_explorer_component_events/run.py b/demo/file_explorer_component_events/run.py
index 0e1022d20b..5292fc9370 100644
--- a/demo/file_explorer_component_events/run.py
+++ b/demo/file_explorer_component_events/run.py
@@ -10,22 +10,18 @@
                         choices=[str(base_root / "dir1"), str(base_root / "dir2"),
                                  str(base_root / "dir3")])
         with gr.Group():
-            dir_only_glob = gr.Checkbox(label="Show only directories", value=False)
-            ignore_dir_in_glob = gr.Checkbox(label="Ignore directories in glob", value=False)
+            txt_only_glob = gr.Checkbox(label="Show only text files", value=False)
+            ignore_txt_in_glob = gr.Checkbox(label="Ignore text files in glob", value=False)
 
     fe = gr.FileExplorer(root=str(base_root / "dir1"),
                          glob="**/*", interactive=True)
     textbox = gr.Textbox(label="Selected Directory")
     run = gr.Button("Run")
     
-    dir_only_glob.select(lambda s: gr.FileExplorer(glob="**/" if s else "**/*.*",
-                                                   file_count="multiple",
-                                                   root=str(base_root / "dir3")) ,
-                         inputs=[dir_only_glob], outputs=[fe])
-    ignore_dir_in_glob.select(lambda s: gr.FileExplorer(glob="**/*",
-                                                        ignore_glob="**/",
-                                                        root=str(base_root / "dir3")),
-                            inputs=[ignore_dir_in_glob], outputs=[fe])            
+    txt_only_glob.select(lambda s: gr.FileExplorer(glob="*.txt" if s else "*") ,
+                         inputs=[txt_only_glob], outputs=[fe])
+    ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob="*.txt" if s else None),
+                            inputs=[ignore_txt_in_glob], outputs=[fe])            
 
     dd.select(lambda s: gr.FileExplorer(root=s), inputs=[dd], outputs=[fe])
     run.click(lambda s: ",".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox])
diff --git a/gradio/components/file_explorer.py b/gradio/components/file_explorer.py
index fb7686b065..763e7858ca 100644
--- a/gradio/components/file_explorer.py
+++ b/gradio/components/file_explorer.py
@@ -2,9 +2,8 @@
 
 from __future__ import annotations
 
-import itertools
+import fnmatch
 import os
-import re
 import warnings
 from pathlib import Path
 from typing import Any, Callable, List, Literal
@@ -33,7 +32,7 @@ class FileExplorer(Component):
 
     def __init__(
         self,
-        glob: str = "**/*.*",
+        glob: str = "**/*",
         *,
         value: str | list[str] | Callable | None = None,
         file_count: Literal["single", "multiple"] = "multiple",
@@ -59,7 +58,7 @@ def __init__(
             value: The file (or list of files, depending on the `file_count` parameter) to show as "selected" when the component is first loaded. If a callable is provided, it will be called when the app loads to set the initial value of the component. If not provided, no files are shown as selected.
             file_count: Whether to allow single or multiple files to be selected. If "single", the component will return a single absolute file path as a string. If "multiple", the component will return a list of absolute file paths as a list of strings.
             root_dir: Path to root directory to select files from. If not provided, defaults to current working directory.
-            ignore_glob: The glob-tyle pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
+            ignore_glob: The glob-style pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
             label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
             every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise.sed (e.g. to cancel it) via this component's .load_event attribute.
             show_label: if True, will display label.
@@ -82,10 +81,10 @@ def __init__(
         self.root_dir = os.path.abspath(root_dir)
         self.glob = glob
         self.ignore_glob = ignore_glob
-        valid_file_count = ["single", "multiple", "directory"]
+        valid_file_count = ["single", "multiple"]
         if file_count not in valid_file_count:
             raise ValueError(
-                f"Invalid value for parameter `type`: {type}. Please choose from one of: {valid_file_count}"
+                f"Invalid value for parameter `file_count`: {file_count}. Please choose from one of: {valid_file_count}"
             )
         self.file_count = file_count
         self.height = height
@@ -156,80 +155,40 @@ def postprocess(self, value: str | list[str] | None) -> FileExplorerData | None:
         return FileExplorerData(root=root)
 
     @server
-    def ls(self, _=None) -> list[dict[str, str]] | None:
+    def ls(self, subdirectory: list | None = None) -> list[dict[str, str]] | None:
         """
         Returns:
             tuple of list of files in directory, then list of folders in directory
         """
+        if subdirectory is None:
+            subdirectory = []
+
+        full_subdir_path = self._safe_join(subdirectory)
+
+        try:
+            subdir_items = sorted(os.listdir(full_subdir_path))
+        except FileNotFoundError:
+            return []
+
+        files, folders = [], []
+        for item in subdir_items:
+            full_path = os.path.join(full_subdir_path, item)
+            is_file = not os.path.isdir(full_path)
+            valid_by_glob = fnmatch.fnmatch(full_path, self.glob)
+            if is_file and not valid_by_glob:
+                continue
+            if self.ignore_glob and fnmatch.fnmatch(full_path, self.ignore_glob):
+                continue
+            target = files if is_file else folders
+            target.append(
+                {
+                    "name": item,
+                    "type": "file" if is_file else "folder",
+                    "valid": valid_by_glob,
+                }
+            )
 
-        def expand_braces(text, seen=None):
-            if seen is None:
-                seen = set()
-
-            spans = [m.span() for m in re.finditer("{[^{}]*}", text)][::-1]
-            alts = [text[start + 1 : stop - 1].split(",") for start, stop in spans]
-
-            if len(spans) == 0:
-                if text not in seen:
-                    yield text
-                seen.add(text)
-
-            else:
-                for combo in itertools.product(*alts):
-                    replaced = list(text)
-                    for (start, stop), replacement in zip(spans, combo):
-                        replaced[start:stop] = replacement
-
-                    yield from expand_braces("".join(replaced), seen)
-
-        def make_tree(files):
-            tree = []
-            for file in files:
-                parts = file.split(os.path.sep)
-                make_node(parts, tree)
-            return tree
-
-        def make_node(parts, tree):
-            _tree = tree
-            for i in range(len(parts)):
-                if _tree is None:
-                    continue
-                if i == len(parts) - 1:
-                    type = "file"
-                    _tree.append({"path": parts[i], "type": type, "children": None})
-                    continue
-                type = "folder"
-                j = next(
-                    (index for (index, v) in enumerate(_tree) if v["path"] == parts[i]),
-                    None,
-                )
-                if j is not None:
-                    _tree = _tree[j]["children"]
-                else:
-                    _tree.append({"path": parts[i], "type": type, "children": []})
-                    _tree = _tree[-1]["children"]
-
-        files: list[Path] = []
-        for result in expand_braces(self.glob):
-            files += list(Path(self.root_dir).resolve().glob(result))
-
-        files = [f for f in files if f != Path(self.root_dir).resolve()]
-
-        ignore_files = []
-        if self.ignore_glob:
-            for result in expand_braces(self.ignore_glob):
-                ignore_files += list(Path(self.root_dir).resolve().glob(result))
-            files = list(set(files) - set(ignore_files))
-
-        files_with_sep = []
-        for f in files:
-            file = str(f.relative_to(self.root_dir))
-            if f.is_dir():
-                file += os.path.sep
-            files_with_sep.append(file)
-
-        tree = make_tree(files_with_sep)
-        return tree
+        return folders + files
 
     def _safe_join(self, folders):
         combined_path = os.path.join(self.root_dir, *folders)
diff --git a/js/app/test/file_explorer_component_events.spec.ts b/js/app/test/file_explorer_component_events.spec.ts
index 99e5fb9028..bd25f97baf 100644
--- a/js/app/test/file_explorer_component_events.spec.ts
+++ b/js/app/test/file_explorer_component_events.spec.ts
@@ -52,9 +52,9 @@ test("File Explorer correctly displays both directories and files. Directories i
 
 	await page
 		.locator("li")
-		.filter({ hasText: "dir4 dir5 dir7 . dir_4_foo.txt" })
+		.filter({ hasText: "dir4 . dir5 dir7 dir_4_bar.log dir_4_foo.txt" })
 		.getByRole("checkbox")
-		.nth(3)
+		.first()
 		.check();
 
 	await page
@@ -97,20 +97,21 @@ test("File Explorer selects all children when top level directory is selected.",
 	const directory_paths_displayed = async () => {
 		const value = await page.getByLabel("Selected Directory").inputValue();
 		const files_and_dirs = value.split(",");
-		expect(files_and_dirs.length).toBe(6);
+		expect(files_and_dirs.length).toBe(7);
 	};
 	await expect(directory_paths_displayed).toPass();
 });
 
-test("File Explorer correctly displays only directories and properly adds it to the value", async ({
-	page
-}) => {
+test("File Explorer correctly displays only text files", async ({ page }) => {
 	const check = page.getByRole("checkbox", {
-		name: "Show only directories",
+		name: "Show only text files",
 		exact: true
 	});
 	await check.click();
 
+	await page.getByLabel("Select File Explorer Root").click();
+	await page.getByLabel(new RegExp("/dir3$"), { exact: true }).first().click();
+
 	await page
 		.locator("span")
 		.filter({ hasText: "dir4" })
@@ -118,25 +119,27 @@ test("File Explorer correctly displays only directories and properly adds it to
 		.check();
 	await page.getByRole("button", { name: "Run" }).click();
 
-	const directory_paths_displayed = async () => {
+	const text_files_displayed = async () => {
 		const value = await page.getByLabel("Selected Directory").inputValue();
 		const dirs = value.split(",");
-		expect(dirs.some((f) => f.endsWith("dir4"))).toBeTruthy();
-		expect(dirs.some((f) => f.endsWith("dir5"))).toBeTruthy();
-		expect(dirs.some((f) => f.endsWith("dir7"))).toBeTruthy();
+		expect(dirs.length).toBe(3);
+		expect(dirs.every((d) => d.endsWith(".txt"))).toBeTruthy();
 	};
-	await expect(directory_paths_displayed).toPass();
+	await expect(text_files_displayed).toPass();
 });
 
-test("File Explorer correctly excludes directories when ignore_glob is '**/'.", async ({
+test("File Explorer correctly excludes text files when ignore_glob is '*.txt'.", async ({
 	page
 }) => {
 	const check = page.getByRole("checkbox", {
-		name: "Ignore directories in glob",
+		name: "Ignore text files in glob",
 		exact: true
 	});
 	await check.click();
 
+	await page.getByLabel("Select File Explorer Root").click();
+	await page.getByLabel(new RegExp("/dir3$"), { exact: true }).first().click();
+
 	await page
 		.locator("span")
 		.filter({ hasText: "dir4" })
@@ -147,10 +150,9 @@ test("File Explorer correctly excludes directories when ignore_glob is '**/'.",
 	const only_files_displayed = async () => {
 		const value = await page.getByLabel("Selected Directory").inputValue();
 		const files = value.split(",");
-		expect(files.length).toBe(3);
-		expect(files.some((f) => f.endsWith("dir_4_foo.txt"))).toBeTruthy();
-		expect(files.some((f) => f.endsWith("dir5_foo.txt"))).toBeTruthy();
-		expect(files.some((f) => f.endsWith("dir7_foo.txt"))).toBeTruthy();
+		expect(files.length).toBe(4);
+		expect(files.some((f) => f.endsWith(".log"))).toBeTruthy();
+		expect(files.some((f) => f.endsWith(".txt"))).toBeFalsy();
 	};
 	await expect(only_files_displayed).toPass();
 });
diff --git a/js/fileexplorer/Index.svelte b/js/fileexplorer/Index.svelte
index e7bb480835..a4c95ac2f5 100644
--- a/js/fileexplorer/Index.svelte
+++ b/js/fileexplorer/Index.svelte
@@ -3,6 +3,7 @@
 <script lang="ts">
 	import type { Gradio, SelectData } from "@gradio/utils";
 	import { File } from "@gradio/icons";
+	import type { FileNode } from "./shared/types";
 
 	import { Block, BlockLabel } from "@gradio/atoms";
 	import DirectoryExplorer from "./shared/DirectoryExplorer.svelte";
@@ -20,11 +21,9 @@
 	export let show_label: boolean;
 	export let height: number | undefined = undefined;
 	export let file_count: "single" | "multiple" = "multiple";
-
+	export let root_dir: string;
 	export let glob: string;
 	export let ignore_glob: string;
-	export let root_dir: string;
-
 	export let loading_status: LoadingStatus;
 	export let container = true;
 	export let scale: number | null = null;
@@ -33,9 +32,11 @@
 		change: never;
 	}>;
 	export let server: {
-		ls: (path: string[]) => Promise<[string[], string[]]>;
+		ls: (path: string[]) => Promise<FileNode[]>;
 	};
 	export let interactive: boolean;
+
+	$: rerender_key = [root_dir, glob, ignore_glob];
 </script>
 
 <Block
@@ -62,14 +63,13 @@
 		label={label || "FileExplorer"}
 		float={false}
 	/>
-	<DirectoryExplorer
-		bind:value
-		{file_count}
-		{server}
-		{interactive}
-		{root_dir}
-		{glob}
-		{ignore_glob}
-		on:change={() => gradio.dispatch("change")}
-	/>
+	{#key rerender_key}
+		<DirectoryExplorer
+			bind:value
+			{file_count}
+			{interactive}
+			ls_fn={server.ls}
+			on:change={() => gradio.dispatch("change")}
+		/>
+	{/key}
 </Block>
diff --git a/js/fileexplorer/shared/Checkbox.svelte b/js/fileexplorer/shared/Checkbox.svelte
index 0a9fd0dd95..23edd6ac1b 100644
--- a/js/fileexplorer/shared/Checkbox.svelte
+++ b/js/fileexplorer/shared/Checkbox.svelte
@@ -7,10 +7,8 @@
 
 <input
 	bind:checked={value}
+	on:input={() => dispatch("change", !value)}
 	type="checkbox"
-	on:click={() => dispatch("change", value)}
-	on:keydown={({ key }) =>
-		(key === " " || key === "Enter") && dispatch("change", value)}
 	{disabled}
 	class:disabled={disabled && !value}
 />
@@ -47,15 +45,6 @@
 	}
 
 	.disabled {
-		cursor: not-allowed;
-		border-color: var(--checkbox-border-color-hover);
-		background-color: var(--checkbox-background-color-hover);
-	}
-
-	input:disabled:checked,
-	input:disabled:checked:hover,
-	.disabled:checked:focus {
-		opacity: 0.8 !important;
-		cursor: not-allowed;
+		opacity: 0;
 	}
 </style>
diff --git a/js/fileexplorer/shared/DirectoryExplorer.svelte b/js/fileexplorer/shared/DirectoryExplorer.svelte
index 754dbff5d7..2b4ace8d05 100644
--- a/js/fileexplorer/shared/DirectoryExplorer.svelte
+++ b/js/fileexplorer/shared/DirectoryExplorer.svelte
@@ -1,72 +1,65 @@
 <script lang="ts">
-	import { createEventDispatcher } from "svelte";
-	import { dequal } from "dequal";
 	import FileTree from "./FileTree.svelte";
-	import { make_fs_store } from "./utils";
-	import { File } from "@gradio/icons";
-	import { Empty } from "@gradio/atoms";
+	import type { FileNode } from "./types";
 
-	export let glob: string;
-	export let ignore_glob: string;
-	export let root_dir: string;
 	export let interactive: boolean;
-	export let server: any;
 	export let file_count: "single" | "multiple" = "multiple";
-
 	export let value: string[][] = [];
+	export let ls_fn: (path: string[]) => Promise<FileNode[]>;
+	let selected_folders: string[][] = [];
 
-	const dispatch = createEventDispatcher<{
-		change: typeof value;
-	}>();
-	const tree = make_fs_store();
-
-	const render_tree = (): void => {
-		server.ls().then((v: any) => {
-			tree.create_fs_graph(v);
-		});
+	const paths_equal = (path: string[], path_2: string[]): boolean => {
+		return path.join("/") === path_2.join("/");
 	};
 
-	$: glob, ignore_glob, root_dir, render_tree();
-
-	$: value.length && $tree && set_checked_from_paths();
-
-	function set_checked_from_paths(): void {
-		value = file_count === "single" ? [value[0] || []] : value;
-		value = tree.set_checked_from_paths(value);
-		if (!dequal(value, old_value)) {
-			old_value = value;
-			dispatch("change", value);
-		}
-	}
+	const path_in_set = (path: string[], set: string[][]): boolean => {
+		return set.some((x) => paths_equal(x, path));
+	};
 
-	let old_value: typeof value = [];
-	function handle_select({
-		node_indices,
-		checked
-	}: {
-		node_indices: number[];
-		checked: boolean;
-	}): void {
-		value = tree.set_checked(node_indices, checked, value, file_count);
-		if (!dequal(value, old_value)) {
-			old_value = value;
-			dispatch("change", value);
-		}
-	}
+	const path_inside = (path: string[], path_2: string[]): boolean => {
+		return path.join("/").startsWith(path_2.join("/"));
+	};
 </script>
 
-{#if $tree && $tree.length}
-	<div class="file-wrap">
-		<FileTree
-			tree={$tree}
-			{interactive}
-			on:check={({ detail }) => handle_select(detail)}
-			{file_count}
-		/>
-	</div>
-{:else}
-	<Empty size="large"><File /></Empty>
-{/if}
+<div class="file-wrap">
+	<FileTree
+		path={[]}
+		selected_files={value}
+		{selected_folders}
+		{interactive}
+		{ls_fn}
+		{file_count}
+		valid_for_selection={false}
+		on:check={(e) => {
+			const { path, checked, type } = e.detail;
+			if (checked) {
+				if (file_count === "single") {
+					value = [path];
+				} else if (type === "folder") {
+					if (!path_in_set(path, selected_folders)) {
+						selected_folders = [...selected_folders, path];
+					}
+				} else {
+					if (!path_in_set(path, value)) {
+						value = [...value, path];
+					}
+				}
+			} else {
+				selected_folders = selected_folders.filter(
+					(folder) => !path_inside(path, folder)
+				); // deselect all parent folders
+				if (type === "folder") {
+					selected_folders = selected_folders.filter(
+						(folder) => !path_inside(folder, path)
+					); // deselect all children folders
+					value = value.filter((file) => !path_inside(file, path)); // deselect all children files
+				} else {
+					value = value.filter((x) => !paths_equal(x, path));
+				}
+			}
+		}}
+	/>
+</div>
 
 <style>
 	.file-wrap {
diff --git a/js/fileexplorer/shared/FileTree.svelte b/js/fileexplorer/shared/FileTree.svelte
index 99c4c56a4c..f58d5fa9e7 100644
--- a/js/fileexplorer/shared/FileTree.svelte
+++ b/js/fileexplorer/shared/FileTree.svelte
@@ -1,76 +1,129 @@
 <script lang="ts">
-	import type { Node } from "./utils";
-	import { createEventDispatcher, tick } from "svelte";
+	import type { FileNode } from "./types";
+	import { createEventDispatcher } from "svelte";
 
 	import Arrow from "./ArrowIcon.svelte";
 	import Checkbox from "./Checkbox.svelte";
 	import FileIcon from "../icons/light-file.svg";
 	import FolderIcon from "../icons/light-folder.svg";
 
+	export let path: string[] = [];
+	export let selected_files: string[][] = [];
+	export let selected_folders: string[][] = [];
+	export let is_selected_entirely = false;
 	export let interactive: boolean;
-	export let tree: Node[] = [];
-	export let icons: any = {};
-	export let node_indices: number[] = [];
+	export let ls_fn: (path: string[]) => Promise<FileNode[]>;
 	export let file_count: "single" | "multiple" = "multiple";
+	export let valid_for_selection: boolean;
+
+	let content: FileNode[] = [];
+	let opened_folders: number[] = [];
+
+	const toggle_open_folder = (i: number): void => {
+		if (opened_folders.includes(i)) {
+			opened_folders = opened_folders.filter((x) => x !== i);
+		} else {
+			opened_folders = [...opened_folders, i];
+		}
+	};
+
+	const open_folder = (i: number): void => {
+		if (!opened_folders.includes(i)) {
+			opened_folders = [...opened_folders, i];
+		}
+	};
+
+	(async () => {
+		content = await ls_fn(path);
+		if (valid_for_selection) {
+			content = [{ name: ".", type: "file" }, ...content];
+		}
+		opened_folders = content
+			.map((x, i) =>
+				x.type === "folder" &&
+				(is_selected_entirely || selected_files.some((y) => y[0] === x.name))
+					? i
+					: null
+			)
+			.filter((x): x is number => x !== null);
+	})();
+
+	$: if (is_selected_entirely) {
+		content.forEach((x) => {
+			dispatch("check", {
+				path: [...path, x.name],
+				checked: true,
+				type: x.type
+			});
+		});
+	}
 
 	const dispatch = createEventDispatcher<{
-		check: { node_indices: number[]; checked: boolean };
+		check: { path: string[]; checked: boolean; type: "file" | "folder" };
 	}>();
-
-	async function dispatch_change(i: number): Promise<void> {
-		await tick();
-
-		dispatch("check", {
-			node_indices: [...node_indices, i],
-			checked: !tree[i].checked
-		});
-	}
 </script>
 
 <ul>
-	{#each tree as { type, path, children, children_visible, checked }, i}
+	{#each content as { type, name, valid }, i}
 		<li>
 			<span class="wrap">
 				<Checkbox
 					disabled={!interactive ||
 						(type === "folder" && file_count === "single")}
-					bind:value={checked}
-					on:change={() => dispatch_change(i)}
+					value={(type === "file" ? selected_files : selected_folders).some(
+						(x) => x[0] === name && x.length === 1
+					)}
+					on:change={(e) => {
+						let checked = e.detail;
+						dispatch("check", {
+							path: [...path, name],
+							checked,
+							type
+						});
+						if (type === "folder" && checked) {
+							open_folder(i);
+						}
+					}}
 				/>
 
 				{#if type === "folder"}
 					<span
 						class="icon"
-						class:hidden={!tree[i].children_visible}
-						on:click|stopPropagation={() =>
-							(tree[i].children_visible = !tree[i].children_visible)}
+						class:hidden={!opened_folders.includes(i)}
+						on:click|stopPropagation={() => toggle_open_folder(i)}
 						role="button"
 						aria-label="expand directory"
 						tabindex="0"
-						on:keydown={({ key }) =>
-							(key === " " || key === "Enter") &&
-							(tree[i].children_visible = !tree[i].children_visible)}
-						><Arrow /></span
+						on:keydown={({ key }) => {
+							if (key === " " || key === "Enter") {
+								toggle_open_folder(i);
+							}
+						}}><Arrow /></span
 					>
-				{:else if path === ""}
-					<span class="file-icon">
-						<img src={FolderIcon} alt="folder icon" />
-					</span>
 				{:else}
 					<span class="file-icon">
-						<img src={FileIcon} alt="file icon" />
+						<img src={name === "." ? FolderIcon : FileIcon} alt="file icon" />
 					</span>
 				{/if}
-				{path ? path : "."}
+				{name}
 			</span>
-			{#if children && children_visible}
+			{#if type === "folder" && opened_folders.includes(i)}
 				<svelte:self
-					tree={children}
-					{icons}
-					on:check
-					node_indices={[...node_indices, i]}
+					path={[...path, name]}
+					selected_files={selected_files
+						.filter((x) => x[0] === name)
+						.map((x) => x.slice(1))}
+					selected_folders={selected_folders
+						.filter((x) => x[0] === name)
+						.map((x) => x.slice(1))}
+					is_selected_entirely={selected_folders.some(
+						(x) => x[0] === name && x.length === 1
+					)}
 					{interactive}
+					{ls_fn}
 					{file_count}
+					valid_for_selection={valid}
+					on:check
 				/>
 			{/if}
 		</li>
diff --git a/js/fileexplorer/shared/types.ts b/js/fileexplorer/shared/types.ts
new file mode 100644
index 0000000000..609c94262d
--- /dev/null
+++ b/js/fileexplorer/shared/types.ts
@@ -0,0 +1,5 @@
+export interface FileNode {
+	type: "file" | "folder";
+	name: string;
+	valid?: boolean;
+}
diff --git a/js/fileexplorer/shared/utils.test.ts b/js/fileexplorer/shared/utils.test.ts
deleted file mode 100644
index 531597e51c..0000000000
--- a/js/fileexplorer/shared/utils.test.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import { describe, test, expect, beforeAll } from "vitest";
-import { make_fs_store, type SerialisedNode } from "./utils";
-import { get } from "svelte/store";
-
-describe("fs_store", () => {
-	let store: ReturnType<typeof make_fs_store>,
-		n0: SerialisedNode,
-		n1: SerialisedNode,
-		n2: SerialisedNode,
-		n3: SerialisedNode,
-		n4: SerialisedNode,
-		n5: SerialisedNode;
-	beforeAll(() => {
-		n4 = {
-			type: "file",
-			path: "d.txt",
-			parent: null,
-			children: []
-		};
-		n5 = {
-			type: "file",
-			path: "e.txt",
-			parent: null,
-			children: []
-		};
-
-		n3 = {
-			type: "folder",
-			path: "c",
-			parent: null,
-			children: [n4, n5]
-		};
-
-		n2 = {
-			type: "folder",
-			path: "b",
-			parent: null,
-			children: [n3]
-		};
-
-		n1 = {
-			type: "folder",
-			path: "a",
-			parent: null,
-			children: [n2]
-		};
-
-		n0 = {
-			type: "file",
-			path: "my-file.txt",
-			parent: null,
-			children: []
-		};
-
-		store = make_fs_store();
-	});
-
-	test("initialise store with correct references", () => {
-		store.create_fs_graph([n1, n0]);
-
-		const items = get(store);
-
-		expect(items?.[0].last).toEqual(items?.[1]);
-		expect(items?.[1].last).toEqual(items?.[1]);
-		expect(items?.[1].previous).toEqual(items?.[0]);
-		expect(items?.[0].previous).toEqual(null);
-		expect(items?.[0].parent).toEqual(null);
-		expect(items?.[0].children?.[0].parent).toEqual(items?.[0]);
-	});
-
-	test("set_checked_from_paths", () => {
-		const checked_paths = [
-			["a", "b", "c", "d.txt"],
-			["a", "b", "c", "e.txt"]
-		];
-		const new_checked_paths = store.set_checked_from_paths(checked_paths);
-
-		const items = get(store);
-
-		expect(new_checked_paths).toEqual(checked_paths);
-		expect(items?.[0].checked).toEqual(true);
-	});
-
-	test("set_checked_from_paths should be deterministic", () => {
-		const checked_paths = [
-			["a", "b", "c", "d.txt"],
-			["a", "b", "c", "e.txt"]
-		];
-		const new_checked_paths = store.set_checked_from_paths(checked_paths);
-
-		const items = get(store);
-
-		expect(new_checked_paths).toEqual(checked_paths);
-		expect(items?.[0].checked).toEqual(true);
-	});
-
-	test("set_checked should check the appropriate index", () => {
-		const checked_indices = [0, 0, 0, 0];
-		store.set_checked(checked_indices, false, [], "multiple");
-
-		const items = get(store);
-
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[0].checked
-		).toEqual(false);
-	});
-
-	test("if all children are set to false then all parents should also be false", () => {
-		const checked_indices = [0, 0, 0, 1];
-		store.set_checked(checked_indices, false, [], "multiple");
-
-		const items = get(store);
-
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[0].checked
-		).toEqual(false);
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[1].checked
-		).toEqual(false);
-		expect(items?.[0].children?.[0].children?.[0].checked).toEqual(false);
-		expect(items?.[0].children?.[0].checked).toEqual(false);
-		expect(items?.[0].checked).toEqual(false);
-	});
-
-	test("if only one child is set to true then parent should be false", () => {
-		const checked_indices = [0, 0, 0, 1];
-		store.set_checked(checked_indices, true, [], "multiple");
-
-		const items = get(store);
-
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[0].checked
-		).toEqual(false);
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[1].checked
-		).toEqual(true);
-		expect(items?.[0].children?.[0].children?.[0].checked).toEqual(false);
-		expect(items?.[0].children?.[0].checked).toEqual(false);
-		expect(items?.[0].checked).toEqual(false);
-	});
-
-	test("if all children are set to true then parents should be true", () => {
-		const checked_indices = [0, 0, 0, 0];
-		store.set_checked(checked_indices, true, [], "multiple");
-
-		const items = get(store);
-
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[0].checked
-		).toEqual(true);
-		expect(
-			items?.[0].children?.[0].children?.[0].children?.[1].checked
-		).toEqual(true);
-		expect(items?.[0].children?.[0].children?.[0].checked).toEqual(true);
-		expect(items?.[0].children?.[0].checked).toEqual(true);
-		expect(items?.[0].checked).toEqual(true);
-	});
-
-	test("calling set_checked multiple times should not impact other nodes", () => {
-		store.set_checked([1], true, [], "multiple");
-		expect(get(store)?.[1].checked).toEqual(true);
-
-		store.set_checked([0], true, [], "multiple");
-		expect(get(store)?.[1].checked).toEqual(true);
-
-		store.set_checked([0], false, [], "multiple");
-		expect(get(store)?.[1].checked).toEqual(true);
-
-		store.set_checked([0], true, [], "multiple");
-		expect(get(store)?.[1].checked).toEqual(true);
-
-		store.set_checked([0], false, [], "multiple");
-		expect(get(store)?.[1].checked).toEqual(true);
-
-		const items = get(store);
-
-		// expect(
-		// 	items?.[0].children?.[0].children?.[0].children?.[0].checked
-		// ).toEqual(true);
-		// expect(
-		// 	items?.[0].children?.[0].children?.[0].children?.[1].checked
-		// ).toEqual(true);
-		// expect(items?.[0].children?.[0].children?.[0].checked).toEqual(true);
-		// expect(items?.[0].children?.[0].checked).toEqual(true);
-		// expect(items?.[0].checked).toEqual(true);
-	});
-});
diff --git a/js/fileexplorer/shared/utils.ts b/js/fileexplorer/shared/utils.ts
deleted file mode 100644
index 95d7d6f40d..0000000000
--- a/js/fileexplorer/shared/utils.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import { writable, type Readable } from "svelte/store";
-import { dequal } from "dequal";
-
-export interface Node {
-	type: "file" | "folder";
-	path: string;
-	children?: Node[];
-	checked: boolean;
-	children_visible: boolean;
-	last?: Node | null;
-	parent: Node | null;
-	previous?: Node | null;
-}
-
-export type SerialisedNode = Omit<
-	Node,
-	"checked" | "children_visible" | "children"
-> & { children?: SerialisedNode[] };
-
-interface FSStore {
-	subscribe: Readable<Node[] | null>["subscribe"];
-	create_fs_graph: (serialised_node: SerialisedNode[]) => void;
-
-	set_checked: (
-		indices: number[],
-		checked: boolean,
-		checked_paths: string[][],
-		file_count: "single" | "multiple"
-	) => string[][];
-	set_checked_from_paths: (checked_paths: string[][]) => string[][];
-}
-
-export const make_fs_store = (): FSStore => {
-	const { subscribe, set } = writable<Node[] | null>(null);
-	let root: Node = {
-		type: "folder",
-		path: "",
-		checked: false,
-		children_visible: false,
-		parent: null
-	};
-	let tree_updated = false;
-
-	function create_fs_graph(serialised_node: SerialisedNode[]): void {
-		root.children = process_tree(serialised_node);
-		tree_updated = true;
-		set(root.children);
-	}
-
-	let old_checked_paths: string[][] = [];
-
-	function set_checked_from_paths(checked_paths: string[][]): string[][] {
-		if (dequal(checked_paths, old_checked_paths) && !tree_updated) {
-			return checked_paths;
-		}
-		old_checked_paths = checked_paths;
-		check_node_and_children(root.children, false, []);
-		const new_checked_paths: string[][] = [];
-		const seen_nodes = new Set();
-		for (let i = 0; i < checked_paths.length; i++) {
-			let _node = root;
-			let _path = [];
-			for (let j = 0; j < checked_paths[i].length; j++) {
-				if (!_node?.children) {
-					continue;
-				}
-				_path.push(checked_paths[i][j]);
-				_node = _node.children!.find((v) => v.path === checked_paths[i][j])!;
-			}
-
-			if (!_node) {
-				continue;
-			}
-
-			_node.checked = true;
-			ensure_visible(_node);
-			const nodes = check_node_and_children(_node.children, true, [_node]);
-			check_parent(_node);
-			nodes.forEach((node) => {
-				const path = get_full_path(node);
-				const normalized_path = path.join("/");
-				// let normalized_path = path.join("/");
-				// normalized_path = normalized_path.endsWith("/") ? normalized_path.slice(0, -1) : normalized_path;
-				if (node.type === "file") {
-					if (seen_nodes.has(normalized_path)) {
-						return;
-					}
-					new_checked_paths.push(path);
-					seen_nodes.add(normalized_path);
-				}
-			});
-		}
-
-		set(root.children!);
-
-		tree_updated = false;
-
-		return new_checked_paths;
-	}
-
-	function set_checked(
-		indices: number[],
-		checked: boolean,
-		checked_paths: string[][],
-		file_count: "single" | "multiple"
-	): string[][] {
-		let _node = root;
-
-		if (file_count === "single") {
-			check_node_and_children(root.children, false, []);
-			set(root.children!);
-		}
-
-		for (let i = 0; i < indices.length; i++) {
-			_node = _node.children![indices[i]];
-		}
-
-		_node.checked = checked;
-		const nodes = check_node_and_children(_node.children, checked, [_node]);
-
-		let new_checked_paths = new Map(checked_paths.map((v) => [v.join("/"), v]));
-
-		for (let i = 0; i < nodes.length; i++) {
-			const _path = get_full_path(nodes[i]);
-			if (!nodes[i].checked) {
-				new_checked_paths.delete(_path.join("/"));
-			} else if (nodes[i].checked) {
-				if (file_count === "single") {
-					new_checked_paths = new Map();
-				}
-
-				new_checked_paths.set(_path.join("/"), _path);
-			}
-		}
-
-		check_parent(_node);
-
-		set(root.children!);
-		old_checked_paths = Array.from(new_checked_paths).map((v) => v[1]);
-		return old_checked_paths;
-	}
-
-	return {
-		subscribe,
-		create_fs_graph,
-		set_checked,
-		set_checked_from_paths
-	};
-};
-
-function ensure_visible(node: Node): void {
-	if (node.parent) {
-		node.parent.children_visible = true;
-		ensure_visible(node.parent);
-	}
-}
-
-function process_tree(
-	node: SerialisedNode[],
-	depth = 0,
-	path_segments: string[] = [],
-	parent: Node | null = null
-): Node[] {
-	const folders: Node[] = [];
-	const files: Node[] = [];
-
-	for (let i = 0; i < node.length; i++) {
-		let n: (typeof node)[number] = node[i];
-
-		if (n.type === "file") {
-			let index = files.findIndex(
-				(v) => v.path.toLocaleLowerCase() >= n.path.toLocaleLowerCase()
-			);
-
-			const _node: Node = {
-				children: undefined,
-				type: "file",
-				path: n.path,
-				checked: false,
-				children_visible: false,
-				parent: parent
-			};
-
-			files.splice(index === -1 ? files.length : index, 0, _node);
-		} else {
-			let index = folders.findIndex(
-				(v) => v.path.toLocaleLowerCase() >= n.path.toLocaleLowerCase()
-			);
-
-			const _node: Node = {
-				type: "folder",
-				path: n.path,
-				checked: false,
-				children_visible: false,
-				parent: parent
-			};
-
-			const children = process_tree(
-				n.children!,
-				depth + 1,
-				[...path_segments, n.path],
-				_node
-			);
-
-			_node.children = children;
-
-			folders.splice(index === -1 ? folders.length : index, 0, _node);
-		}
-	}
-
-	const last = files[files.length - 1] || folders[folders.length - 1];
-
-	for (let i = 0; i < folders.length; i++) {
-		folders[i].last = last;
-		folders[i].previous = folders[i - 1] || null;
-	}
-
-	for (let i = 0; i < files.length; i++) {
-		if (i === 0) {
-			files[i].previous = folders[folders.length - 1] || null;
-		} else {
-			files[i].previous = files[i - 1] || null;
-		}
-		files[i].last = last;
-	}
-
-	return Array().concat(folders, files);
-}
-
-function get_full_path(node: Node, path: string[] = []): string[] {
-	const new_path = [node.path, ...path];
-
-	if (node.parent) {
-		return get_full_path(node.parent, new_path);
-	}
-	return new_path;
-}
-
-function check_node_and_children(
-	node: Node[] | null | undefined,
-	checked: boolean,
-	checked_nodes: Node[]
-): Node[] {
-	if (node === null || node === undefined) return checked_nodes;
-	for (let i = 0; i < node.length; i++) {
-		node[i].checked = checked;
-		checked_nodes.push(node[i]);
-		if (checked) ensure_visible(node[i]);
-
-		checked_nodes.concat(
-			check_node_and_children(node[i].children, checked, checked_nodes)
-		);
-	}
-
-	return checked_nodes;
-}
-
-function check_parent(node: Node | null | undefined): void {
-	if (node === null || node === undefined || !node.parent) return;
-	let _node = node.last;
-	let nodes_checked = [];
-	while (_node) {
-		nodes_checked.push(_node.checked);
-		_node = _node.previous;
-	}
-	if (nodes_checked.every((v) => v === true)) {
-		node.parent!.checked = true;
-		check_parent(node?.parent);
-	} else if (nodes_checked.some((v) => v === false)) {
-		node.parent!.checked = false;
-		check_parent(node?.parent);
-	}
-}
diff --git a/test/test_components.py b/test/test_components.py
index 5078c292b0..cc22214bf2 100644
--- a/test/test_components.py
+++ b/test/test_components.py
@@ -2874,7 +2874,7 @@ def test_component_functions(self):
         file_explorer = gr.FileExplorer(file_count="single")
 
         config = file_explorer.get_config()
-        assert config["glob"] == "**/*.*"
+        assert config["glob"] == "**/*"
         assert config["value"] is None
         assert config["file_count"] == "single"
         assert config["server_fns"] == ["ls"]
@@ -2891,7 +2891,7 @@ def test_component_functions(self):
         file_explorer = gr.FileExplorer(file_count="multiple")
 
         config = file_explorer.get_config()
-        assert config["glob"] == "**/*.*"
+        assert config["glob"] == "**/*"
         assert config["value"] is None
         assert config["file_count"] == "multiple"
         assert config["server_fns"] == ["ls"]
@@ -2905,66 +2905,24 @@ def test_component_functions(self):
         preprocessed_data = file_explorer.preprocess(input_data)
         assert preprocessed_data == []
 
-    def test_file_explorer_dir_only_glob(self, tmpdir):
+    def test_file_explorer_txt_only_glob(self, tmpdir):
         tmpdir.mkdir("foo")
-        tmpdir.mkdir("bar")
-        tmpdir.mkdir("baz")
-        (Path(tmpdir) / "baz" / "qux").mkdir()
-        (Path(tmpdir) / "foo" / "abc").mkdir()
-        (Path(tmpdir) / "foo" / "abc" / "def").mkdir()
-        (Path(tmpdir) / "foo" / "abc" / "def" / "file.txt").touch()
-
-        file_explorer = gr.FileExplorer(glob="**/", root=Path(tmpdir))
-        tree = file_explorer.ls()
-
-        def sort_answer(answer):
-            answer = sorted(answer, key=lambda x: x["path"])
-            for item in answer:
-                if item["children"]:
-                    item["children"] = sort_answer(item["children"])
-            return answer
+        (Path(tmpdir) / "foo" / "bar").mkdir()
+        (Path(tmpdir) / "foo" / "file.txt").touch()
+        (Path(tmpdir) / "foo" / "file2.txt").touch()
+        (Path(tmpdir) / "foo" / "file3.log").touch()
+        (Path(tmpdir) / "foo" / "img.png").touch()
+        (Path(tmpdir) / "foo" / "bar" / "bar.txt").touch()
+
+        file_explorer = gr.FileExplorer(glob="*.txt", root=Path(tmpdir))
+        tree = file_explorer.ls(["foo"])
 
         answer = [
-            {
-                "path": "bar",
-                "type": "folder",
-                "children": [{"path": "", "type": "file", "children": None}],
-            },
-            {
-                "path": "baz",
-                "type": "folder",
-                "children": [
-                    {"path": "", "type": "file", "children": None},
-                    {
-                        "path": "qux",
-                        "type": "folder",
-                        "children": [{"path": "", "type": "file", "children": None}],
-                    },
-                ],
-            },
-            {
-                "path": "foo",
-                "type": "folder",
-                "children": [
-                    {"path": "", "type": "file", "children": None},
-                    {
-                        "path": "abc",
-                        "type": "folder",
-                        "children": [
-                            {"path": "", "type": "file", "children": None},
-                            {
-                                "path": "def",
-                                "type": "folder",
-                                "children": [
-                                    {"path": "", "type": "file", "children": None}
-                                ],
-                            },
-                        ],
-                    },
-                ],
-            },
+            {"name": "bar", "type": "folder", "valid": False},
+            {"name": "file.txt", "type": "file", "valid": True},
+            {"name": "file2.txt", "type": "file", "valid": True},
         ]
-        assert sort_answer(tree) == sort_answer(answer)
+        assert tree == answer
 
 
 def test_component_class_ids():
diff --git a/test/test_utils.py b/test/test_utils.py
index d6d191ef03..383a576ea6 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -7,7 +7,6 @@
 from pathlib import Path
 from unittest.mock import MagicMock, patch
 
-import httpx
 import pytest
 from typing_extensions import Literal
 
@@ -20,6 +19,7 @@
     check_function_inputs_match,
     colab_check,
     delete_none,
+    download_if_url,
     get_continuous_fn,
     get_extension_from_file_path_or_url,
     get_type_hints,
@@ -27,7 +27,6 @@
     is_in_or_equal,
     is_special_typed_parameter,
     kaggle_check,
-    download_if_url,
     sagemaker_check,
     sanitize_list_for_csv,
     sanitize_value_for_csv,


Code review comments:
diff_hunk: @@ -10,22 +10,18 @@
                         choices=[str(base_root / "dir1"), str(base_root / "dir2"),
                                  str(base_root / "dir3")])
         with gr.Group():
-            dir_only_glob = gr.Checkbox(label="Show only directories", value=False)
-            ignore_dir_in_glob = gr.Checkbox(label="Ignore directories in glob", value=False)
+            txt_only_glob = gr.Checkbox(label="Show only text files", value=False)
+            ignore_txt_in_glob = gr.Checkbox(label="Ignore text files in glob", value=False)
 
     fe = gr.FileExplorer(root=str(base_root / "dir1"),
path: demo/file_explorer_component_events/run.py
text: @user1:
```suggestion
    fe = gr.FileExplorer(root_dir=str(base_root / "dir1"),
```

diff_hunk: @@ -156,80 +155,40 @@ def postprocess(self, value: str | list[str] | None) -> FileExplorerData | None:
         return FileExplorerData(root=root)
 
     @server
-    def ls(self, _=None) -> list[dict[str, str]] | None:
+    def ls(self, subdirectory: list | None = None) -> list[dict[str, str]] | None:
         """
         Returns:
             tuple of list of files in directory, then list of folders in directory
path: gradio/components/file_explorer.py
text: @user1:
```suggestion
            a list of dictionaries, where each dictionary represents a file or subdirectory in the given subdirectory
```

diff_hunk: @@ -59,7 +58,7 @@ def __init__(
             value: The file (or list of files, depending on the `file_count` parameter) to show as "selected" when the component is first loaded. If a callable is provided, it will be called when the app loads to set the initial value of the component. If not provided, no files are shown as selected.
             file_count: Whether to allow single or multiple files to be selected. If "single", the component will return a single absolute file path as a string. If "multiple", the component will return a list of absolute file paths as a list of strings.
             root_dir: Path to root directory to select files from. If not provided, defaults to current working directory.
-            ignore_glob: The glob-tyle pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
+            ignore_glob: The glob-style pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
path: gradio/components/file_explorer.py
text: @user1:
I would add "case-sensitive" here and above, as files on Windows are not case-sensitive, but fnmatch.fnmatch is
```suggestion
            ignore_glob: The glob-style, case-sensitive pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
```