From 648bcb10b89dac60b5c47a8e670890021d89e404 Mon Sep 17 00:00:00 2001 From: BoyuShen2004 <156837090+Boyu-Shen@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:49:57 -0400 Subject: [PATCH] (1) longer YAML file dropbar for model training. (2) Add a button to delete all uploads and a button to clear all mounted projects. (3) Better animation at the receibing folder when dragging a file into the folder. (4) Robust clean up of old processes for scripts/start.sh. (5) Select checkpoint file instead of directory for model inference --- client/package-lock.json | 17 -- client/src/components/InputSelector.js | 2 +- client/src/components/YamlFileUploader.js | 2 +- client/src/views/FilesManager.js | 205 +++++++++++++++++++++- scripts/start.sh | 53 ++++++ server_api/auth/router.py | 141 +++++++++++++++ 6 files changed, 398 insertions(+), 22 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index bda968f..51c229d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18762,23 +18762,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", diff --git a/client/src/components/InputSelector.js b/client/src/components/InputSelector.js index 0598374..860f5e5 100644 --- a/client/src/components/InputSelector.js +++ b/client/src/components/InputSelector.js @@ -197,7 +197,7 @@ function InputSelector(props) { placeholder="Model checkpoint file (e.g., /path/to/checkpoint_00010.pth.tar)" value={context.checkpointPath || ""} onChange={handleCheckpointPathChange} - selectionType="directory" + selectionType="file" /> )} diff --git a/client/src/components/YamlFileUploader.js b/client/src/components/YamlFileUploader.js index 03d958f..2aa677e 100644 --- a/client/src/components/YamlFileUploader.js +++ b/client/src/components/YamlFileUploader.js @@ -350,7 +350,7 @@ const YamlFileUploader = (props) => { { + confirmText = e.target.value; + if (finalConfirmModal) { + finalConfirmModal.update({ + okButtonProps: { + danger: true, + disabled: confirmText.trim() !== "DELETE", + }, + }); + } + }} + /> + + ), + okText: "Delete All Uploads", + okButtonProps: { danger: true, disabled: true }, + cancelText: "Cancel", + onOk: async () => { + if (confirmText.trim() !== "DELETE") { + return; + } + try { + const res = await apiClient.delete("/files/uploads/all", { + withCredentials: true, + }); + const refreshed = await fetchFiles(); + const folderStillExists = + currentFolder === "root" || + refreshed?.folders?.some((folder) => folder.key === currentFolder); + if (!folderStillExists) { + handleNavigate("root"); + } + setSelectedItems([]); + const deletedCount = res?.data?.deleted_count ?? 0; + if (deletedCount > 0) { + message.success(`Deleted ${deletedCount} uploaded item(s).`); + } else { + message.info("No uploaded files/folders found."); + } + } catch (err) { + console.error("Delete all uploads error", err); + message.error("Failed to delete uploaded files"); + } + }, + }); + }, + }); + }; + + const handleClearAllMountedProjects = () => { + Modal.confirm({ + title: "Clear all mounted projects?", + content: + "This removes mounted project indexes from explorer. Source files on disk are not deleted.", + okText: "Continue", + okButtonProps: { danger: true }, + cancelText: "Cancel", + onOk: async () => { + let confirmText = ""; + let finalConfirmModal = null; + finalConfirmModal = Modal.confirm({ + title: "Final confirmation required", + content: ( +
+
+ Type CLEAR to confirm removing all mounted + project indexes. +
+ { + confirmText = e.target.value; + if (finalConfirmModal) { + finalConfirmModal.update({ + okButtonProps: { + danger: true, + disabled: confirmText.trim() !== "CLEAR", + }, + }); + } + }} + /> +
+ ), + okText: "Clear Mounted Projects", + okButtonProps: { danger: true, disabled: true }, + cancelText: "Cancel", + onOk: async () => { + if (confirmText.trim() !== "CLEAR") { + return; + } + try { + const res = await apiClient.delete("/files/mounted/all", { + withCredentials: true, + }); + const deletedCount = res?.data?.deleted_count ?? 0; + await fetchFiles(); + handleNavigate("root"); + setSelectedItems([]); + if (deletedCount > 0) { + message.success(`Cleared ${deletedCount} mounted project(s).`); + } else { + message.info("No mounted projects found."); + } + } catch (err) { + console.error("Clear mounted projects error", err); + message.error("Failed to clear mounted projects"); + } + }, + }); + }, + }); + }; + return (
Mount Project + +
{/* Content Area */} @@ -1475,6 +1673,7 @@ function FilesManager() { onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onDragOver={handleDragOver} + onDragLeave={() => setDragOverFolderKey(null)} onDrop={(e) => handleDrop(e, currentFolder)} > {currentFolders.length === 0 && diff --git a/scripts/start.sh b/scripts/start.sh index af92e17..d6dacde 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -10,6 +10,9 @@ export OLLAMA_EMBED_MODEL="qwen3-embedding:8b" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CLIENT_DIR="${ROOT_DIR}/client" +PORTS=(8000 4242 4243 3000) +PIDS=() +CLEANED_UP=0 if ! command -v uv >/dev/null 2>&1; then echo "uv is required. Install it from https://docs.astral.sh/uv/." >&2 @@ -21,18 +24,68 @@ if ! command -v npm >/dev/null 2>&1; then exit 1 fi +kill_port_listeners() { + local port="$1" + local pids + pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)" + if [[ -z "${pids}" ]]; then + return + fi + + echo "Stopping stale process(es) on port ${port}: ${pids}" + kill ${pids} 2>/dev/null || true + sleep 0.3 + + local stubborn + stubborn="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)" + if [[ -n "${stubborn}" ]]; then + echo "Force killing stubborn process(es) on port ${port}: ${stubborn}" + kill -9 ${stubborn} 2>/dev/null || true + fi +} + +cleanup() { + if [[ "${CLEANED_UP}" -eq 1 ]]; then + return + fi + CLEANED_UP=1 + + echo "Shutting down background services..." + for pid in "${PIDS[@]:-}"; do + if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then + kill "${pid}" 2>/dev/null || true + fi + done + + sleep 0.3 + for port in "${PORTS[@]}"; do + kill_port_listeners "${port}" + done +} + +trap cleanup EXIT INT TERM + +echo "Cleaning stale listeners before startup..." +for port in "${PORTS[@]}"; do + kill_port_listeners "${port}" +done + echo "Starting data server (port 8000)..." uv run --directory "${ROOT_DIR}" python server_api/scripts/serve_data.py & +PIDS+=("$!") echo "Starting API server (port 4242)..." PYTHONDONTWRITEBYTECODE=1 uv run --directory "${ROOT_DIR}" python -m server_api.main & +PIDS+=("$!") echo "Starting PyTC server (port 4243)..." uv run --directory "${ROOT_DIR}" python -m server_pytc.main & +PIDS+=("$!") echo "Starting React server (port 3000)..." pushd "${CLIENT_DIR}" >/dev/null BROWSER=none npm start >/dev/null 2>&1 & +PIDS+=("$!") wait_for_react() { local max_attempts=60 diff --git a/server_api/auth/router.py b/server_api/auth/router.py index e00b9c5..7c746b5 100644 --- a/server_api/auth/router.py +++ b/server_api/auth/router.py @@ -549,6 +549,147 @@ def unmount_project( return {"message": "Project unmounted"} +@router.delete("/files/uploads/all") +def delete_all_uploaded_files( + current_user: models.User = Depends(get_current_user), + db: Session = Depends(database.get_db), +): + """ + Remove uploads + app-created virtual entries only. + Mounted project indexes are not affected. + """ + all_nodes = db.query(models.File).filter(models.File.user_id == current_user.id).all() + if not all_nodes: + return {"message": "No uploaded files found.", "deleted_count": 0} + + def parent_key(node): + return str(node.path) if node.path is not None else "root" + + nodes_by_id = {str(node.id): node for node in all_nodes} + children_by_parent = {} + for node in all_nodes: + children_by_parent.setdefault(parent_key(node), []).append(node) + + # Mounted entries (outside managed uploads) and their ancestors are protected. + protected_ids = set() + mounted_ids = { + str(node.id) + for node in all_nodes + if node.physical_path + and not _is_managed_upload_path(current_user.id, node.physical_path) + } + for mounted_id in mounted_ids: + current_id = mounted_id + while current_id and current_id != "root" and current_id not in protected_ids: + protected_ids.add(current_id) + parent = nodes_by_id.get(current_id) + if not parent: + break + current_id = parent_key(parent) + + # Removable: app virtual nodes or managed-upload nodes, excluding protected tree. + removable_ids = { + str(node.id) + for node in all_nodes + if ( + (not node.physical_path) + or _is_managed_upload_path(current_user.id, node.physical_path) + ) + and str(node.id) not in protected_ids + } + if not removable_ids: + return {"message": "No uploaded files found.", "deleted_count": 0} + + deleted_count = 0 + + def delete_subtree(node_id: str): + nonlocal deleted_count + node = nodes_by_id.get(node_id) + if not node: + return + + for child in children_by_parent.get(node_id, []): + child_id = str(child.id) + if child_id in removable_ids: + delete_subtree(child_id) + + # Keep folders that still contain protected/non-removable children. + if node.is_folder and any( + str(child.id) not in removable_ids + for child in children_by_parent.get(node_id, []) + ): + return + + if ( + not node.is_folder + and node.physical_path + and os.path.exists(node.physical_path) + and _is_managed_upload_path(current_user.id, node.physical_path) + ): + os.remove(node.physical_path) + + db.delete(node) + deleted_count += 1 + + root_ids = [node_id for node_id in removable_ids if parent_key(nodes_by_id[node_id]) not in removable_ids] + for node_id in root_ids: + delete_subtree(node_id) + + # Reset uploads dir after cleanup. + uploads_root = os.path.abspath(os.path.join("uploads", str(current_user.id))) + if os.path.isdir(uploads_root): + shutil.rmtree(uploads_root, ignore_errors=True) + os.makedirs(uploads_root, exist_ok=True) + + db.commit() + return { + "message": f"Deleted {deleted_count} upload item(s).", + "deleted_count": deleted_count, + } + + +@router.delete("/files/mounted/all") +def delete_all_mounted_projects( + current_user: models.User = Depends(get_current_user), + db: Session = Depends(database.get_db), +): + """ + Remove all mounted project indexes for the current user. + Source files on disk are not deleted. + """ + root_folders = ( + db.query(models.File) + .filter( + models.File.user_id == current_user.id, + models.File.is_folder.is_(True), + models.File.path == "root", + ) + .all() + ) + # Only mounted roots: physical path exists and points outside managed uploads. + target_roots = [ + node + for node in root_folders + if node.physical_path + and not _is_managed_upload_path(current_user.id, node.physical_path) + ] + + if not target_roots: + return {"message": "No mounted projects found.", "deleted_count": 0} + + deleted_count = 0 + for node in target_roots: + # Index-only removal; source files on disk remain untouched. + _delete_file_tree(db, current_user.id, node, delete_disk_files=False) + deleted_count += 1 + + db.commit() + return { + "message": f"Removed {deleted_count} mounted project(s).", + "deleted_count": deleted_count, + } + + @router.delete("/files/{file_id}") def delete_file( file_id: int,