From 0155abd0da44e1bab475f3fbe9ca298798394cf8 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 18:55:25 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20b=E1=BB=95=20sung=20ph=E1=BA=A7n=20prof?= =?UTF-8?q?ile=20c=E1=BB=A7a=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/audio.py | 29 +++++++++++++++++- app/static/js/app.jsx | 46 +++++++++++++++++++++------- app/static/js/app.precompiled.js | 52 ++++++++++++++++++++++++-------- 3 files changed, 103 insertions(+), 24 deletions(-) diff --git a/app/api/v1/audio.py b/app/api/v1/audio.py index b1474c8..f347176 100644 --- a/app/api/v1/audio.py +++ b/app/api/v1/audio.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, UploadFile, File, HTTPException, Query, Depends from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Optional, List +import json from app.config import settings from app.api.v1.auth import get_current_user from app.api.v1.projects import get_optional_user @@ -67,7 +68,16 @@ async def upload_audio(file: UploadFile = File(...), current_user: Optional[dict with open(file_path, "wb") as f: content = await file.read() f.write(content) - + + # Save original filename as sidecar metadata + import json + meta_path = os.path.join(settings.UPLOADS_DIR, file_id + ".meta") + try: + with open(meta_path, "w") as mf: + json.dump({"original_name": file.filename}, mf) + except Exception: + pass + # Trigger celery task from app.tasks.worker import analyze_audio_task task = analyze_audio_task.delay(file_id) @@ -357,8 +367,18 @@ async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_ if filename in files_map: files_map[filename]["size_mb"] = round(files_map[filename]["size_mb"] + stat.st_size / (1024 * 1024), 2) else: + original_name = filename + meta_path = os.path.join(directory, filename + ".meta") + if os.path.isfile(meta_path): + try: + with open(meta_path, "r") as mf: + meta = json.load(mf) + original_name = meta.get("original_name", filename) + except Exception: + pass files_map[filename] = { "file_id": filename, + "original_name": original_name, "size_mb": round(stat.st_size / (1024 * 1024), 2), "created_at": stat.st_mtime, "type": type_label, @@ -390,6 +410,13 @@ async def delete_user_file(file_id: str, current_user: dict = Depends(get_curren deleted = True except Exception: pass + # Clean up sidecar metadata file + meta_path = os.path.join(directory, file_id + ".meta") + if os.path.isfile(meta_path): + try: + os.remove(meta_path) + except Exception: + pass if not deleted: raise HTTPException(status_code=404, detail="Không tìm thấy tệp trên server") diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 1f75093..74413ae 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -2752,6 +2752,9 @@ const ProfileModal = ({ const [filesList, setFilesList] = useState([]); const [loadingFiles, setLoadingFiles] = useState(false); + // Confirmation modal state + const [confirmModal, setConfirmModal] = useState(null); + useEffect(() => { if (isOpen) { fetchProfile(); @@ -2821,15 +2824,20 @@ const ProfileModal = ({ const handleDeleteProject = async (projectId, e) => { e.stopPropagation(); - if (!confirm("Bạn có chắc chắn muốn xóa dự án này khỏi Cloud?")) return; - try { - await window.SonicAPI.deleteCloudProject(projectId); - showToast("Đã xóa dự án thành công!", "success"); - fetchProjects(); - fetchProfile(); - } catch (err) { - showToast(err.message || "Lỗi khi xóa dự án", "error"); - } + setConfirmModal({ + title: "Xóa dự án Cloud", + message: "Bạn có chắc chắn muốn xóa dự án này khỏi Cloud? Hành động này không thể hoàn tác.", + onConfirm: async () => { + try { + await window.SonicAPI.deleteCloudProject(projectId); + showToast("Đã xóa dự án thành công!", "success"); + fetchProjects(); + fetchProfile(); + } catch (err) { + showToast(err.message || "Lỗi khi xóa dự án", "error"); + } + } + }); }; const handleDeleteFile = async (fileId) => { @@ -2883,8 +2891,24 @@ const ProfileModal = ({ }; return /*#__PURE__*/React.createElement("div", { - className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" + className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm relative" + }, confirmModal && /*#__PURE__*/React.createElement("div", { + className: "absolute inset-0 z-[60] flex items-center justify-center bg-black/50" }, /*#__PURE__*/React.createElement("div", { + className: "bg-[#262626] border border-[#383838] rounded-lg shadow-2xl p-5 max-w-sm w-full text-slate-200" + }, /*#__PURE__*/React.createElement("h4", { + className: "text-sm font-bold text-rose-400 mb-2" + }, confirmModal.title), /*#__PURE__*/React.createElement("p", { + className: "text-xs text-slate-300 mb-4" + }, confirmModal.message), /*#__PURE__*/React.createElement("div", { + className: "flex justify-end gap-2" + }, /*#__PURE__*/React.createElement("button", { + onClick: () => setConfirmModal(null), + className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold" + }, "Hủy"), /*#__PURE__*/React.createElement("button", { + onClick: () => { const fn = confirmModal.onConfirm; setConfirmModal(null); fn(); }, + className: "px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold" + }, "Xác nhận xóa")))), /*#__PURE__*/React.createElement("div", { className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200 flex flex-col max-h-[85vh]" }, /*#__PURE__*/React.createElement("div", { className: "flex justify-between items-center pb-3 border-b border-[#383838] shrink-0" @@ -3024,7 +3048,7 @@ const ProfileModal = ({ className: "flex items-center justify-between p-3 bg-[#1e1e1e] rounded border border-zinc-800 text-xs" }, [ /*#__PURE__*/React.createElement("div", { key: "meta", className: "max-w-[70%]" }, [ - /*#__PURE__*/React.createElement("div", { className: "font-mono font-semibold text-slate-300 truncate" }, file.file_id.replace(/^user_[^_]+_/, '')), + /*#__PURE__*/React.createElement("div", { className: "font-semibold text-slate-300 truncate" }, file.original_name || file.file_id), /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 mt-0.5" }, `Loại: ${file.type} | Dung lượng: ${file.size_mb} MB | Tạo lúc: ${new Date(file.created_at * 1000).toLocaleString()}`) ]), /*#__PURE__*/React.createElement("div", { key: "actions", className: "flex items-center gap-2" }, [ diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 533dde9..18d6913 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -2788,6 +2788,9 @@ const ProfileModal = ({ // Files list state const [filesList, setFilesList] = useState([]); const [loadingFiles, setLoadingFiles] = useState(false); + + // Confirmation modal state + const [confirmModal, setConfirmModal] = useState(null); useEffect(() => { if (isOpen) { fetchProfile(); @@ -2852,15 +2855,20 @@ const ProfileModal = ({ }; const handleDeleteProject = async (projectId, e) => { e.stopPropagation(); - if (!confirm("Bạn có chắc chắn muốn xóa dự án này khỏi Cloud?")) return; - try { - await window.SonicAPI.deleteCloudProject(projectId); - showToast("Đã xóa dự án thành công!", "success"); - fetchProjects(); - fetchProfile(); - } catch (err) { - showToast(err.message || "Lỗi khi xóa dự án", "error"); - } + setConfirmModal({ + title: "Xóa dự án Cloud", + message: "Bạn có chắc chắn muốn xóa dự án này khỏi Cloud? Hành động này không thể hoàn tác.", + onConfirm: async () => { + try { + await window.SonicAPI.deleteCloudProject(projectId); + showToast("Đã xóa dự án thành công!", "success"); + fetchProjects(); + fetchProfile(); + } catch (err) { + showToast(err.message || "Lỗi khi xóa dự án", "error"); + } + } + }); }; const handleDeleteFile = async fileId => { if (!confirm(`Bạn có chắc chắn muốn xóa tệp tin ${fileId}?`)) return; @@ -2910,8 +2918,28 @@ const ProfileModal = ({ } }; return /*#__PURE__*/React.createElement("div", { - className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" + className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm relative" + }, confirmModal && /*#__PURE__*/React.createElement("div", { + className: "absolute inset-0 z-[60] flex items-center justify-center bg-black/50" }, /*#__PURE__*/React.createElement("div", { + className: "bg-[#262626] border border-[#383838] rounded-lg shadow-2xl p-5 max-w-sm w-full text-slate-200" + }, /*#__PURE__*/React.createElement("h4", { + className: "text-sm font-bold text-rose-400 mb-2" + }, confirmModal.title), /*#__PURE__*/React.createElement("p", { + className: "text-xs text-slate-300 mb-4" + }, confirmModal.message), /*#__PURE__*/React.createElement("div", { + className: "flex justify-end gap-2" + }, /*#__PURE__*/React.createElement("button", { + onClick: () => setConfirmModal(null), + className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold" + }, "Hủy"), /*#__PURE__*/React.createElement("button", { + onClick: () => { + const fn = confirmModal.onConfirm; + setConfirmModal(null); + fn(); + }, + className: "px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold" + }, "Xác nhận xóa")))), /*#__PURE__*/React.createElement("div", { className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200 flex flex-col max-h-[85vh]" }, /*#__PURE__*/React.createElement("div", { className: "flex justify-between items-center pb-3 border-b border-[#383838] shrink-0" @@ -3067,8 +3095,8 @@ const ProfileModal = ({ key: "meta", className: "max-w-[70%]" }, [/*#__PURE__*/React.createElement("div", { - className: "font-mono font-semibold text-slate-300 truncate" - }, file.file_id.replace(/^user_[^_]+_/, '')), /*#__PURE__*/React.createElement("div", { + className: "font-semibold text-slate-300 truncate" + }, file.original_name || file.file_id), /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 mt-0.5" }, `Loại: ${file.type} | Dung lượng: ${file.size_mb} MB | Tạo lúc: ${new Date(file.created_at * 1000).toLocaleString()}`)]), /*#__PURE__*/React.createElement("div", { key: "actions",