I am creating a webui with FastAPI as an api backend, and REACT as the frontend, I have a few endpoints for managing images and image captions in the database in the backend, in the REACT side, I have an axios set up as apiClient, with these utility functions:
```
// DATASET ENDPOINTS
export const imageAdd = async (image_file) => {
    return apiClient.post('/dataset/image/add/', { image_file });
}

export const imageAddPath = async (image_path) => {
    return apiClient.post('/dataset/image/add_path/', { image_path });
}

export const imagesGetByHash = async (ahash) => {
    return apiClient.get(`/dataset/image/by-hash/${ahash}`);
}

export const imagesSearchPaged = async (search, page=0, limit=10) => {
    return apiClient.get('/dataset/image/search/', { search, page, limit });
}

export const imageCaptionAdd = async (image_id, caption_source_id, caption) => {
    return apiClient.post(`/dataset/image/${image_id}/caption/add/`, { image_caption: { caption_source_id, caption }});
}

export const imageCaptionGetIdByImageId = async (image_id, image_caption_id) => {
    return apiClient.get(`/dataset/image/${image_id}/caption/${image_caption_id}`);
}

export const imageCaptionGetPagedByImageId = async (image_id, page=0, limit=10) => {
    return apiClient.get(`/dataset/image/${image_id}/caption/`, { page, limit });
}

export const imageGetById = async (image_id) => {
    return apiClient.get(`/dataset/image/${image_id}`);
}

export const imagesGetPaged = async (page=0, limit=10) => {
    return apiClient.get('/dataset/image/', { page, limit });
}

export const imageGetFile = async (image_id, width=null, height=null, resize_mode='fit') => {
    return fileClient.get(`/dataset/image-file/${image_id}`, { width, height, resize_mode });
}

export const captionSourceAdd = async (name) => {
    return apiClient.post('/dataset/caption_source/add/', { caption_source: { name }});
}

export const captionSourceGet = async (caption_source_id) => {
    return apiClient.get(`/dataset/caption_source/${caption_source_id}`);
}

export const captionSourcesGetAll = async () => {
    return apiClient.get('/dataset/caption_source/');
}

export const imageCaptionSearchPaged = async (search, page=0, limit=10) => {
    return apiClient.get('/dataset/image_caption/search/', { search, page, limit });
}

export const imageCaptionGetById = async (image_caption_id) => {
    return apiClient.get(`/dataset/image_caption/${image_caption_id}`);
}

export const imageCaptionsGetPaged = async (page=0, limit=10) => {
    return apiClient.get('/dataset/image_caption/', { page, limit });
}
```

I am not currently using them, but that is fine, I plan to slowly change over to using them over time, but this is my main page for dataset management, in dataset.jsx:
```
import { useState, useEffect } from "react";
import { Flex, Pagination, Tabs, Input, Button } from "antd";
import axios from "axios";
import "./dataset.css";
import ImageGrid from "../components/imagegrid.jsx";
const { Search } = Input;
const { TabPane } = Tabs;

/* 
- Top bar - Handles search, filters, sorting, might also handle swapping data sources(folder/website)
- Left side - This is the list of current images, allow single and multi select, paginated, has its own scrollbar
- Right side - The control panel: Selected info, captioning, dataset(management for datasets), management(file management, actions, bulk actions)
*/

const Dataset = () => {
  const [cpActiveTabKey, setCpActiveTabKey] = useState("info");

  const [searchedText, setSearchedText] = useState("");
  const [images, setImages] = useState([]);
  // Keep track of the last selected index for shift-click functionality
  const [lastSelectedIndex, setLastSelectedIndex] = useState(null);
  const [selectedImageIds, setSelectedImageIds] = useState(new Set());
  const [page, setPage] = useState(1);
  const [limit, setLimit] = useState(10);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    async function fetchImages() {
      try {
        const response = await axios.get(
          `http://localhost:8000/api/v1/dataset/image/search?search=${searchedText}&page=${
            page - 1
          }&limit=${limit}`
        );

        let images = response.data.items;
        images.forEach((image) => {
          image.thumbnailSrc = `http://localhost:8000/api/v1/dataset/image-file/${image.id}?width=150&height=150`;
          image.imageSrc = `http://localhost:8000/api/v1/dataset/image-file/${image.id}`;
          image.captions = ["thumbnail"]; // TODO: We need to get the caption...maybe? Maybe an endpoint that returns images + all captions
        });
        setImages(images);
        setTotal(response.data.total);
      } catch (error) {
        // TODO: Handle errors
        console.error("Error fetching images: ", error);

        if (error.response) {
          // Server responded with a status code that falls out of the range of 2xx
          console.error(error.response.data);
          console.error(error.response.status);
          console.error(error.response.headers);
        } else if (error.request) {
          // The request was made but no response was received
          console.error(error.request);
        } else {
          // Something happened in setting up the request that triggered an error
          console.error("Error", error.message);
        }
      }
    }

    fetchImages();
  }, [searchedText, page, limit]);

  useEffect(() => {
    const fetchCaptions = async (imageId) => {
      try {
        const response = await axios.get(
          `http://localhost:8000/api/v1/dataset/image/${imageId}/caption`
        );

        return response.data.items;
      } catch (error) {
        console.error("Error fetching captions: ", error);
        return [];
      }
    };

    const populateCaptions = async () => {
      const promises = Array.from(selectedImageIds).map(async (imageId) => {
        const captions = await fetchCaptions(imageId);
        return { imageId, captions };
      });

      Promise.all(promises).then((results) => {
        setImages((prevImages) =>
          prevImages.map((image) => {
            const foundCaptions = results.find(
              (result) => result.imageId === image.id
            );
            return foundCaptions
              ? { ...image, captions: foundCaptions.captions }
              : image;
          })
        );
      });
    };

    populateCaptions();
  }, [selectedImageIds]);

  const toggleSelection = (imageId, index, event) => {
    const newSelectedImageIds = new Set(selectedImageIds);

    if (event.shiftKey && lastSelectedIndex !== null) {
      // Shift selection logic
      const start = Math.min(lastSelectedIndex, index);
      const end = Math.max(lastSelectedIndex, index);
      for (let i = start; i <= end; i++) {
        newSelectedImageIds.add(images[i].id);
      }
    } else if (event.ctrlKey) {
      // Control selection logic
      if (newSelectedImageIds.has(imageId)) {
        newSelectedImageIds.delete(imageId);
      } else {
        newSelectedImageIds.add(imageId);
      }
      setLastSelectedIndex(index); // Update the last selected index
    } else {
      // Regular click selection logic
      if (newSelectedImageIds.size === 1 && newSelectedImageIds.has(imageId)) {
        newSelectedImageIds.delete(imageId);
      } else {
        newSelectedImageIds.clear();
        newSelectedImageIds.add(imageId);
        setLastSelectedIndex(index); // Update the last selected index
      }
    }

    setSelectedImageIds(newSelectedImageIds);
  };

  // Select all images
  const selectAll = () => {
    const newSelectedImageIds = new Set();
    images.forEach((image) => newSelectedImageIds.add(image.id));
    setSelectedImageIds(newSelectedImageIds);
  };

  // Deselect all images
  const deselectAll = () => {
    setSelectedImageIds(new Set());
  };

  // Toggle select all images
  const toggleSelect = () => {
    if (selectedImageIds.size === images.length) {
      deselectAll();
    } else {
      selectAll();
    }
  };

  const onTabChange = (key) => {
    setCpActiveTabKey(key);
  };

  const selectedImages = images.filter((image) =>
    selectedImageIds.has(image.id)
  );

  return (
    <Flex className=".content" align="start" vertical>
      {/* === TOP BAR === */}
      {/* 
      So we want the main image source:
        - Local - Uses the database, can add local paths to add images from, maybe store those paths in a config for scanning
          - Sub selection - local paths to scan
        - Web - This just lets you browse the images + tag/captions, multiselect to import into a local path(and thus the DB)
                unsure if I want to put any of this in the db at this point, however I should be able to run captioning tools
                on the images to see what it gives.
                Can also just bulk import all based on the current search/filters.
          - Sub selection - the web source(s) to use, this might be hard to implement within pagination, etc
      Filtering:
        - Width, <=, =, >=
        - Height, <=, =, >=
        - Aspect ratio, fixed selection? 4:3, 16:9, 1:1, 9:16, 3:4, or maybe just: square, portrait, landscape?
      Sorting:
        - Sort by date, id, score, width, height; Asc, Desc
      
      TODO: Maybe have a set image folder, for image storage, and for importing from web sources etc.
       */}
      <Flex>
        <Search
          id="inputSearchBox"
          placeholder="Caption Search"
          allowClear
          onSearch={(value) => {
            if (searchedText !== value) {
              setPage(1);
              setSearchedText(value);
            }
          }}
          style={{
            width: 400,
          }}
        />
        <Flex>
          <div>
            <Button onClick={selectAll}>Select All</Button>
            <Button onClick={deselectAll}>Deselect All</Button>
            <Button onClick={toggleSelect}>Toggle Select</Button>
          </div>
        </Flex>
      </Flex>

      {/* === IMAGE GRID === */}
      <Flex style={{ width: "100%", height: "100%" }}>
        <Flex vertical style={{ width: "60%", height: "100%" }}>
          <Flex className="image-grid-pagination">
            <Pagination
              current={page}
              pageSize={limit}
              total={total}
              onChange={(page, pageSize) => {
                setPage(page);
                setLimit(pageSize);
              }}
              showSizeChanger
              onShowSizeChange={(current, size) => {
                setLimit(size);
              }}
              pageSizeOptions={["10", "20", "50", "100"]}
            />
          </Flex>
          <ImageGrid
            images={images}
            selectedImageIds={selectedImageIds}
            toggleSelection={toggleSelection}
          />
        </Flex>

        {/* === CONTROL PANEL === */}
        <Flex
          vertical
          style={{ width: "40%", height: "100%", backgroundColor: "#f0f0f0" }}
        >
          <Flex className="tabs-container">
            <Tabs
              className="centered-tabs"
              activeKey={cpActiveTabKey}
              onChange={onTabChange}
            >
              <TabPane
                className="image-info"
                tab="Info"
                key="info"
                style={{ backgroundColor: "#f0f0f0" }}
              >
                <ul>
                  {selectedImages.map((image, index) => (
                    <li key={image.id}>
                      <h3>Image ID: {image.id}</h3>
                      {image.captions && image.captions.length > 0 ? (
                        <ul>
                          {image.captions.map((caption, captionIndex) => (
                            <li key={captionIndex}>{caption.caption}</li>
                          ))}
                        </ul>
                      ) : (
                        <p>No captions available</p>
                      )}
                    </li>
                  ))}
                </ul>
              </TabPane>
              <TabPane tab="Captioning" key="captioning">
                Captioning Tab
              </TabPane>
              <TabPane tab="Dataset" key="dataset">
                Dataset Tab
              </TabPane>
              <TabPane tab="Tools" key="tools">
                Tools Tab
              </TabPane>
            </Tabs>
          </Flex>
        </Flex>
      </Flex>
    </Flex>
  );
};

export default Dataset;
```

What I want to do, is on the Tools TabPane, I want to add some utility UI for adding images through those FastAPI endpoints, I want a dialog select to select images to add, through the imageAdd, or pop up a dialog to select a folder to scan for images in the folder by using imageAddPath, now, the FastAPI uses local paths only, we don't upload the images through the endpoint, however I am designing this to be run on the same computer as the browser, a local server, not to be run remotely, so we can just use the browsers open file/folder dialogs.
I would also like to have the ability to manage caption sources, and image captions, that will be handled in the captioning TabPane.
I don't currently have ways to update existing image captions, or caption sources, but you can use the other apiClient functions to predict how they would look, I just want to focus on the REACT side right now, I can add extra endpoints on the FastAPI side myself.
So to start with, let's add functionality on the Tools TabPane for adding images and image paths.