diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 2d94295..fbed2b2 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -4563,6 +4563,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot window.addEventListener('mouseup', up); return () => window.removeEventListener('mouseup', up); }, []); + React.useEffect(() => { + const handler = (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'a') { + const target = e.target; + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return; + e.preventDefault(); + setSelectedNoteIds(notes.map(n => n.id)); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [notes, setSelectedNoteIds]); const NoteHeight = 18; const PITCH_START = 0; // C0 (render all 128 keys) @@ -5065,7 +5077,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot mode: 'move', idx: clickedNoteIdx, startOffsetBeat: beat - clickedNote.start_beat, - startOffsetPitch: pitch - clickedNote.pitch, + startOffsetPitch: pitch, selectedNotesOffset: selectedNotesOffset }); } else { @@ -5127,7 +5139,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot const maxPitch = Math.max(marquee.startPitch, marquee.currentPitch); const insideIds = notes - .filter(n => n.start_beat >= minBeat && n.start_beat <= maxBeat && n.pitch >= minPitch && n.pitch <= maxPitch) + .filter(n => { + const withinPitch = n.pitch >= minPitch && n.pitch <= maxPitch; + if (!withinPitch) return false; + const noteEnd = n.start_beat + n.duration_beats; + if (marquee.startBeat <= marquee.currentBeat) { + // Left to right: select if any overlap + return n.start_beat <= maxBeat && noteEnd >= minBeat; + } else { + // Right to left: select only if fully covered + return n.start_beat >= minBeat && noteEnd <= maxBeat; + } + }) .map(n => n.id); setSelectedNoteIds(insideIds); @@ -5252,14 +5275,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot const firstNote = notes.find(n => n.id === firstOffset.id); if (!firstNote) return; const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapVal) - firstOffset.originalStartBeat; - const deltaPitch = Math.round(pitch - draggedNote.startOffsetPitch) - firstOffset.originalPitch; + // Clamp so no note goes past beat 0 + const minOrigStart = Math.min(...draggedNote.selectedNotesOffset.map(o => o.originalStartBeat)); + const clampedDeltaBeat = minOrigStart + deltaBeat < 0 ? -minOrigStart : deltaBeat; + const deltaPitch = Math.round(pitch - draggedNote.startOffsetPitch); setNotes(prev => prev.map(n => { const offset = draggedNote.selectedNotesOffset.find(o => o.id === n.id); if (!offset) return n; return { ...n, - start_beat: getSnapBeat(Math.max(0, offset.originalStartBeat + deltaBeat), snapVal), + start_beat: getSnapBeat(Math.max(0, offset.originalStartBeat + clampedDeltaBeat), snapVal), pitch: Math.max(0, Math.min(127, offset.originalPitch + deltaPitch)) }; })); @@ -10836,7 +10862,8 @@ const App = () => { const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4; const marginBar = maxDurationRef.current - secondsPerBar; const clampedTime = Math.min(time, marginBar); - const newDuration = Math.max(0.1, clampedTime - resize.originalStart); + const snappedDuration = snapValueRef.current !== 'free' ? snapTime(clampedTime - resize.originalStart, snapValueRef.current, bpm) : clampedTime - resize.originalStart; + const newDuration = Math.max(0.1, snappedDuration); items[idx] = { ...item, duration: newDuration }; } return resize.itemType === 'section' diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 6063e38..9aac427 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -128,7 +128,7 @@ const[filesList,setFilesList]=useState([]);const[loadingFiles,setLoadingFiles]=u const[confirmModal,setConfirmModal]=useState(null);const handleDragStart=e=>{const r=dragRef.current;r.active=true;r.startX=e.clientX;r.startY=e.clientY;r.ofsX=dragOfs.x;r.ofsY=dragOfs.y;const onMove=ev=>{if(!r.active)return;setDragOfs({x:r.ofsX+ev.clientX-r.startX,y:r.ofsY+ev.clientY-r.startY});};const onUp=()=>{r.active=false;document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};useEffect(()=>{if(isOpen){fetchProfile();if(activeTab==='projects')fetchProjects();if(activeTab==='files')fetchFiles();}},[isOpen,activeTab]);const fetchProfile=async()=>{try{const data=await window.SonicAPI.getProfile();setProfile(data);}catch(e){setError(e.message||'Không thể tải thông tin profile');}};const fetchProjects=async()=>{setLoadingProjects(true);try{const data=await window.SonicAPI.listCloudProjects();setProjectsList(data||[]);}catch(e){showToast(e.message||'Không thể tải danh sách dự án','error');}finally{setLoadingProjects(false);}};const fetchFiles=async()=>{setLoadingFiles(true);try{const activeFileIds=tracks.map(t=>t.serverFileId).filter(Boolean);const data=await window.SonicAPI.listMyFiles(activeFileIds);setFilesList(data||[]);}catch(e){showToast(e.message||'Không thể tải danh sách tệp tin','error');}finally{setLoadingFiles(false);}};const handleOpenProject=async projectId=>{setAppWarningModal({title:"Mở dự án",message:"Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",isAlert:false,onConfirm:async()=>{try{const proj=await window.SonicAPI.getCloudProject(projectId);const parsed=JSON.parse(proj.data_json);let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(parsed.main_session){const result=deserializeProjectFromSchema(parsed);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;}else{restoredTracks=(parsed.tracks||[]).map(t=>{const{height:_h,...rest}=t;return{...rest,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null};});}setTracks(restoredTracks);setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(proj.name);setCurrentProjectId(proj.id);if(restoredSessionTabs.length>0){setSessionTabs(restoredSessionTabs);}if(restoredSubTabs.length>0){setSubTabs(restoredSubTabs);}localStorage.setItem('sonic_project_name',proj.name);localStorage.setItem('sonic_project_id',proj.id);showToast(`Đã nạp dự án "${proj.name}" thành công!`,"success");}catch(e){showToast(e.message||"Lỗi khi nạp dự án","error");}}});};const handleDeleteProject=async(projectId,e)=>{e.stopPropagation();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;try{await window.SonicAPI.deleteMyFile(fileId);showToast("Đã xóa tệp tin thành công!","success");fetchFiles();fetchProfile();}catch(err){showToast(err.message||"Lỗi khi xóa tệp tin","error");}};const handleCleanUnusedFiles=async()=>{const unusedFiles=filesList.filter(f=>!f.is_in_use);if(unusedFiles.length===0){showToast("Không có tập tin rác nào để dọn dẹp.","info");return;}if(!confirm(`Bạn có chắc chắn muốn xóa tất cả ${unusedFiles.length} tập tin rác để giải phóng dung lượng?`))return;let successCount=0;for(const file of unusedFiles){try{await window.SonicAPI.deleteMyFile(file.file_id);successCount++;}catch(e){console.error("Lỗi xóa file rác: ",file.file_id,e);}}showToast(`Đã dọn dẹp thành công ${successCount}/${unusedFiles.length} tập tin rác!`,"success");fetchFiles();fetchProfile();};const handleChangePassword=async e=>{e.preventDefault();setMsg('');setError('');setLoading(true);try{const res=await window.SonicAPI.changePassword(oldPassword,newPassword);setMsg(res.message||'Đổi mật khẩu thành công!');setOldPassword('');setNewPassword('');}catch(err){setError(err.message||'Lỗi khi đổi mật khẩu');}finally{setLoading(false);}};const modalStyle={left:`calc(50% + ${dragOfs.x}px)`,top:`calc(50% + ${dragOfs.y}px)`,transform:'translate(-50%, -50%)'};return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{key:"backdrop",className:"fixed inset-0 z-40 bg-black/70 backdrop-blur-sm",onClick:onClose}),confirmModal&&/*#__PURE__*/React.createElement("div",{key:"confirm-overlay",className:"fixed 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",{key:"dialog",className:"fixed z-50 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]",style:modalStyle},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838] shrink-0 cursor-grab active:cursor-grabbing select-none",onMouseDown:handleDragStart},/*#__PURE__*/React.createElement("h3",{className:"text-md font-bold text-teal-400 flex items-center gap-1.5"},"👤 Hồ Sơ Cá Nhân & Quản Lý Dự Án"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),/*#__PURE__*/React.createElement("div",{className:"flex gap-2 border-b border-[#383838] py-2 shrink-0 text-xs font-semibold"},[/*#__PURE__*/React.createElement("button",{key:"tab-acc",onClick:()=>setActiveTab('account'),className:`px-3 py-1.5 rounded transition ${activeTab==='account'?'bg-teal-950/60 text-teal-300 border border-teal-800':'text-slate-400 hover:text-slate-200'}`},"Tài Khoản"),/*#__PURE__*/React.createElement("button",{key:"tab-proj",onClick:()=>setActiveTab('projects'),className:`px-3 py-1.5 rounded transition ${activeTab==='projects'?'bg-teal-950/60 text-teal-300 border border-teal-800':'text-slate-400 hover:text-slate-200'}`},"Dự Án Cloud"),/*#__PURE__*/React.createElement("button",{key:"tab-files",onClick:()=>setActiveTab('files'),className:`px-3 py-1.5 rounded transition ${activeTab==='files'?'bg-teal-950/60 text-teal-300 border border-teal-800':'text-slate-400 hover:text-slate-200'}`},"Tập Tin Của Tôi")]),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto pt-4 space-y-4 pr-1 min-h-[300px]"},activeTab==='account'&&profile?[/*#__PURE__*/React.createElement("div",{key:"quota-info",className:"bg-[#1e1e1e] p-4 rounded-lg border border-[#333] grid grid-cols-2 gap-4 text-xs"},[/*#__PURE__*/React.createElement("div",{key:"username"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Tên người dùng"),/*#__PURE__*/React.createElement("span",{className:"font-bold text-teal-300 text-sm"},profile.username)]),/*#__PURE__*/React.createElement("div",{key:"role"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Vai trò"),/*#__PURE__*/React.createElement("span",{className:"uppercase font-semibold text-amber-400"},profile.role)]),/*#__PURE__*/React.createElement("div",{key:"email"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Email"),/*#__PURE__*/React.createElement("span",null,profile.email)]),/*#__PURE__*/React.createElement("div",{key:"quota"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Dung lượng Quota"),/*#__PURE__*/React.createElement("span",{className:"font-semibold text-slate-200"},`${profile.quota.used_mb} MB / ${profile.quota.storage_limit_mb} MB`)])]),/*#__PURE__*/React.createElement("div",{key:"progress"},[/*#__PURE__*/React.createElement("div",{className:"flex justify-between text-xs mb-1"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Tiến trình sử dụng bộ nhớ Server"),/*#__PURE__*/React.createElement("span",{className:"font-bold text-teal-400"},`${(profile.quota.used_mb/profile.quota.storage_limit_mb*100).toFixed(1)}%`)]),/*#__PURE__*/React.createElement("div",{className:"w-full h-2 bg-slate-800 rounded-full overflow-hidden"},[/*#__PURE__*/React.createElement("div",{className:"h-full bg-teal-500 rounded-full transition-all duration-300",style:{width:`${Math.min(100,profile.quota.used_mb/profile.quota.storage_limit_mb*100)}%`}})])]),/*#__PURE__*/React.createElement("form",{key:"pwd-form",onSubmit:handleChangePassword,className:"pt-4 border-t border-[#383838] space-y-3"},[/*#__PURE__*/React.createElement("h4",{className:"text-xs font-bold text-slate-300 uppercase"},"Thay Đổi Mật Khẩu"),msg&&/*#__PURE__*/React.createElement("div",{className:"p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"},msg),error&&/*#__PURE__*/React.createElement("div",{className:"p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"},error),/*#__PURE__*/React.createElement("div",{key:"old"},[/*#__PURE__*/React.createElement("label",{className:"block text-xs text-slate-400 mb-1"},"Mật khẩu cũ"),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:"current-password",required:true,value:oldPassword,onChange:e=>setOldPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"})]),/*#__PURE__*/React.createElement("div",{key:"new"},[/*#__PURE__*/React.createElement("label",{className:"block text-xs text-slate-400 mb-1"},"Mật khẩu mới"),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:"new-password",required:true,value:newPassword,onChange:e=>setNewPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"})]),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"w-full py-1.5 bg-teal-600 hover:bg-teal-500 text-white font-semibold text-xs rounded transition"},loading?'Đang cập nhật...':'Cập Nhật Mật Khẩu')])]:activeTab==='projects'?[loadingProjects?/*#__PURE__*/React.createElement("div",{key:"loading",className:"text-center py-8 text-xs text-zinc-500"},"Đang tải danh sách dự án..."):projectsList.length===0?/*#__PURE__*/React.createElement("div",{key:"empty",className:"text-center py-8 text-xs text-zinc-500"},"Bạn chưa có dự án nào lưu trên Cloud."):/*#__PURE__*/React.createElement("div",{key:"list",className:"space-y-1.5"},[projectsList.map(proj=>/*#__PURE__*/React.createElement("div",{key:proj.id,onClick:()=>handleOpenProject(proj.id),className:"flex items-center justify-between p-3 bg-[#1e1e1e] hover:bg-zinc-800 rounded border border-zinc-800 transition cursor-pointer text-xs group"},[/*#__PURE__*/React.createElement("div",{key:"meta"},[/*#__PURE__*/React.createElement("div",{className:"font-bold text-slate-200 group-hover:text-teal-400"},proj.name),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 mt-0.5"},`Dung lượng: ${proj.size_mb} MB | Cập nhật: ${new Date(proj.updated_at*1000).toLocaleString()}`)]),/*#__PURE__*/React.createElement("div",{key:"actions",className:"flex items-center gap-1.5"},[/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();handleOpenProject(proj.id);},className:"px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"},"MỞ"),/*#__PURE__*/React.createElement("button",{onClick:e=>handleDeleteProject(proj.id,e),className:"px-2 py-1 bg-rose-950/60 hover:bg-rose-900/80 text-rose-300 rounded font-semibold text-[10px]"},"XÓA")])]))])]:activeTab==='files'?[/*#__PURE__*/React.createElement("div",{key:"cleanup-header",className:"flex items-center justify-between bg-zinc-900/60 p-2.5 rounded border border-zinc-800 text-xs shrink-0 mb-3"},[/*#__PURE__*/React.createElement("span",{className:"text-zinc-400 text-[10px]"},"💡 Các tập tin không dùng trong dự án nào sẽ được đánh dấu 'Không dùng' và có thể xóa để giải phóng dung lượng."),/*#__PURE__*/React.createElement("button",{onClick:handleCleanUnusedFiles,className:"px-2.5 py-1 bg-rose-700 hover:bg-rose-600 text-white rounded font-bold text-[10px] uppercase flex items-center gap-1"},"🧹 Dọn dẹp tệp rác")]),loadingFiles?/*#__PURE__*/React.createElement("div",{key:"loading",className:"text-center py-8 text-xs text-zinc-500"},"Đang tải danh sách tập tin..."):filesList.length===0?/*#__PURE__*/React.createElement("div",{key:"empty",className:"text-center py-8 text-xs text-zinc-500"},"Chưa có tập tin nào tải lên hoặc tạo ra."):/*#__PURE__*/React.createElement("div",{key:"list",className:"space-y-1.5"},[filesList.map(file=>/*#__PURE__*/React.createElement("div",{key:file.file_id,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-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"},[file.is_in_use?/*#__PURE__*/React.createElement("span",{className:"px-2 py-0.5 bg-emerald-950 text-emerald-400 border border-emerald-900 rounded text-[9px] uppercase font-bold font-mono"},"Đang dùng"):/*#__PURE__*/React.createElement("span",{className:"px-2 py-0.5 bg-zinc-850 text-zinc-400 border border-zinc-700 rounded text-[9px] uppercase font-bold font-mono"},"Không dùng"),!file.is_in_use&&/*#__PURE__*/React.createElement("button",{onClick:()=>handleDeleteFile(file.file_id),className:"px-2 py-0.5 bg-rose-950 hover:bg-rose-900 text-rose-300 rounded font-semibold text-[10px] border border-rose-900"},"Xóa")])]))])]:null)));};const SaveProjectModal=({isOpen,onClose,onSave})=>{if(!isOpen)return null;const[name,setName]=useState('');const handleSubmit=e=>{e.preventDefault();if(!name.trim())return;onSave(name.trim());onClose();};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-teal-400 mb-4 uppercase"},"Đặt tên dự án"),/*#__PURE__*/React.createElement("form",{onSubmit:handleSubmit,className:"space-y-4"},[/*#__PURE__*/React.createElement("input",{key:"name-input",type:"text",placeholder:"Nhập tên dự án...",required:true,value:name,onChange:e=>setName(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold",autoFocus:true}),/*#__PURE__*/React.createElement("div",{key:"actions",className:"flex justify-end gap-2 text-xs"},[/*#__PURE__*/React.createElement("button",{key:"cancel",type:"button",onClick:onClose,className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"},"Hủy"),/*#__PURE__*/React.createElement("button",{key:"save",type:"submit",className:"px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold"},"Lưu")])])));};const SaveAsModal=({isOpen,onClose,projectName,onSaveCloud,onSaveLocal})=>{if(!isOpen)return null;const[name,setName]=useState(projectName||'');const[saveType,setSaveType]=useState('cloud');const handleSubmit=e=>{e.preventDefault();if(!name.trim())return;if(saveType==='cloud'){onSaveCloud(name.trim());}else{onSaveLocal(name.trim());}onClose();};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-teal-400 mb-4 uppercase"},"Lưu dưới tên khác (Save As...)"),/*#__PURE__*/React.createElement("form",{onSubmit:handleSubmit,className:"space-y-4"},[/*#__PURE__*/React.createElement("div",{key:"name-block"},[/*#__PURE__*/React.createElement("label",{className:"block text-[10px] text-zinc-400 uppercase font-bold mb-1"},"Tên dự án mới"),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Nhập tên mới...",required:true,value:name,onChange:e=>setName(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold",autoFocus:true})]),/*#__PURE__*/React.createElement("div",{key:"type-block"},[/*#__PURE__*/React.createElement("label",{className:"block text-[10px] text-zinc-400 uppercase font-bold mb-1.5"},"Phương thức lưu trữ"),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs"},[/*#__PURE__*/React.createElement("button",{key:"btn-cloud",type:"button",onClick:()=>setSaveType('cloud'),className:`py-2 rounded border flex flex-col items-center gap-1 font-semibold transition ${saveType==='cloud'?'bg-teal-950/60 border-teal-500 text-teal-300':'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400'}`},[/*#__PURE__*/React.createElement("span",{key:"title"},"☁️ Lưu Cloud"),/*#__PURE__*/React.createElement("span",{key:"desc",className:"text-[9px] font-normal text-zinc-500"},"Lưu lên server cá nhân")]),/*#__PURE__*/React.createElement("button",{key:"btn-local",type:"button",onClick:()=>setSaveType('local'),className:`py-2 rounded border flex flex-col items-center gap-1 font-semibold transition ${saveType==='local'?'bg-teal-950/60 border-teal-500 text-teal-300':'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400'}`},[/*#__PURE__*/React.createElement("span",{key:"title"},"💾 Tải về máy (.sfs)"),/*#__PURE__*/React.createElement("span",{key:"desc",className:"text-[9px] font-normal text-zinc-500"},"Tải tệp JSON dự án về máy")])])]),/*#__PURE__*/React.createElement("div",{key:"actions",className:"flex justify-end gap-2 text-xs pt-2"},[/*#__PURE__*/React.createElement("button",{key:"cancel",type:"button",onClick:onClose,className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"},"Hủy"),/*#__PURE__*/React.createElement("button",{key:"save",type:"submit",className:"px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold"},"Thực hiện lưu")])])));};const SystemManagerModal=({isOpen,onClose})=>{if(!isOpen)return null;const[users,setUsers]=useState([]);const[loading,setLoading]=useState(true);const[msg,setMsg]=useState('');const[error,setError]=useState('');const[editingQuotaUser,setEditingQuotaUser]=useState(null);const[newQuotaMb,setNewQuotaMb]=useState(500);useEffect(()=>{if(isOpen)loadUsers();},[isOpen]);const loadUsers=async()=>{setLoading(true);setError('');try{const data=await window.SonicAPI.listUsers();setUsers(data);}catch(err){setError(err.message||'Không thể tải danh sách người dùng hệ thống');}finally{setLoading(false);}};const handleSaveQuota=async userId=>{try{await window.SonicAPI.updateUserQuota(userId,parseInt(newQuotaMb));setMsg('Đã cập nhật hạn mức Quota thành công!');setEditingQuotaUser(null);loadUsers();}catch(err){setError(err.message||'Lỗi cập nhật Quota');}};const handleToggleRole=async user=>{const nextRole=user.role==='admin'?'standard':'admin';try{await window.SonicAPI.updateUserRole(user.id,nextRole,user.is_active);setMsg(`Đã đổi vai trò người dùng ${user.username} thành ${nextRole}`);loadUsers();}catch(err){setError(err.message||'Lỗi cập nhật vai trò');}};const handleDeleteUser=async userId=>{if(!confirm('Bạn có chắc chắn muốn xóa người dùng này khỏi hệ thống?'))return;try{await window.SonicAPI.deleteUser(userId);setMsg('Đã xóa người dùng thành công');loadUsers();}catch(err){setError(err.message||'Lỗi khi xóa người dùng');}};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-6 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-amber-400"},"⚙️ Quản Lý Hệ Thống & Phân Quyền Admin"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),msg&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"},msg),error&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"},error),/*#__PURE__*/React.createElement("div",{className:"mt-4 overflow-x-auto max-h-96 no-scrollbar"},loading?/*#__PURE__*/React.createElement("div",{className:"py-8 text-center text-slate-400 text-xs"},"Đang tải thông tin hệ thống..."):/*#__PURE__*/React.createElement("table",{className:"w-full text-left text-xs border-collapse"},/*#__PURE__*/React.createElement("thead",null,/*#__PURE__*/React.createElement("tr",{className:"border-b border-[#383838] text-slate-400 bg-[#1e1e1e]"},/*#__PURE__*/React.createElement("th",{className:"p-3"},"Tên Người Dùng"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Email"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Vai Trò"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Dung Lượng Sử Dụng"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Hạn Mức Quota"),/*#__PURE__*/React.createElement("th",{className:"p-3 text-right"},"Thao Tác"))),/*#__PURE__*/React.createElement("tbody",{className:"divide-y divide-[#333]"},users.map(u=>/*#__PURE__*/React.createElement("tr",{key:u.id,className:"hover:bg-[#2e2e2e]"},/*#__PURE__*/React.createElement("td",{className:"p-3 font-semibold text-teal-300"},u.username,u.must_change_password&&/*#__PURE__*/React.createElement("span",{className:"ml-2 text-xs bg-amber-900/60 text-amber-300 px-1.5 py-0.5 rounded"},"Mật khẩu gốc")),/*#__PURE__*/React.createElement("td",{className:"p-3 text-slate-300"},u.email),/*#__PURE__*/React.createElement("td",{className:"p-3 uppercase font-bold text-amber-400"},u.role),/*#__PURE__*/React.createElement("td",{className:"p-3"},u.used_mb," MB"),/*#__PURE__*/React.createElement("td",{className:"p-3"},editingQuotaUser===u.id?/*#__PURE__*/React.createElement("div",{className:"flex items-center space-x-1"},/*#__PURE__*/React.createElement("input",{type:"number",value:newQuotaMb,onChange:e=>setNewQuotaMb(e.target.value),className:"w-16 bg-[#1e1e1e] border border-[#444] rounded px-1 py-0.5 text-xs text-slate-200"}),/*#__PURE__*/React.createElement("span",null,"MB"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSaveQuota(u.id),className:"px-2 py-0.5 bg-teal-600 rounded text-xs"},"Lưu")):/*#__PURE__*/React.createElement("span",{className:"font-semibold"},u.quota_mb," MB")),/*#__PURE__*/React.createElement("td",{className:"p-3 text-right space-x-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingQuotaUser(u.id);setNewQuotaMb(u.quota_mb);},className:"px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-xs"},"Sửa Quota"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleToggleRole(u),className:"px-2 py-1 bg-amber-700/60 hover:bg-amber-600 rounded text-xs"},"Đổi Role"),u.role!=='admin'&&/*#__PURE__*/React.createElement("button",{onClick:()=>handleDeleteUser(u.id),className:"px-2 py-1 bg-red-700/60 hover:bg-red-600 rounded text-xs"},"Xóa")))))))));};const DEFAULT_PRESETS=[{id:"preset_epic_orchestra_intro",name:"Epic Orchestra Intro (8 Bars)",keywords:["epic orchestra","epic orchestral","hoành tráng","nhạc phim epic"],category:"Orchestral / Film Score",default_bars:8,default_bpm:130,default_scale:"C Minor",system_instruction_template:"You are a professional film composer. Create a powerful, dramatic 8-bar orchestral intro. Keep the note density low (e.g. use mostly whole notes, half notes, or quarter notes) and do NOT generate dense 16th notes or complex drum rolls. This is critical to avoid output token limit timeouts. The required structure to return via the `generate_multitrack_midi` tool consists of 3 tracks: 1. Strings: plays smooth legato chord changes (one chord per 1 or 2 bars). 2. Brass Theme: plays a swelling simple melodic line in the C3-C5 range. 3. Epic Percussion: hits heavily on beats 1 and 3. Ensure the duration is precisely 8 bars (32 beats).",is_user_defined:false,created_at:"2026-07-23T16:00:00Z"},{id:"preset_pop_piano_chords",name:"Pop Piano Chords (4 Bars)",keywords:["pop piano","piano chords","ballad piano","hợp âm piano"],category:"Pop / Ballad",default_bars:4,default_bpm:90,default_scale:"C Major",system_instruction_template:"You are a professional Pop Piano player. Generate a beautiful 4-bar piano chord progression (e.g. C - G - Am - F) with pleasant chord voicing and simple accompaniment. Return the MIDI notes via `generate_multitrack_midi` function on a track named 'Pop Piano'. Keep notes simple, using mostly whole/half/quarter notes. Ensure the duration of the track is precisely 4 bars (16 beats).",is_user_defined:false,created_at:"2026-07-23T16:00:00Z"},{id:"preset_cyberpunk_synth",name:"Cyberpunk Synthwave (8 Bars)",keywords:["cyberpunk synth","synthwave","cyberpunk","futuristic synth"],category:"Electronic / Synthwave",default_bars:8,default_bpm:120,default_scale:"A Minor",system_instruction_template:"You are a Synthwave producer. Generate a driving 8-bar cyberpunk synth theme. Return MIDI notes via `generate_multitrack_midi` containing: 1. Synth Bass: eighth notes on pitch A1, C2, G1. 2. Synth Lead: simple melodic line in high register C4-E5. Keep notes clean and concise to ensure fast generation.",is_user_defined:false,created_at:"2026-07-23T16:00:00Z"}];const AIPresetModal=({isOpen,onClose})=>{if(!isOpen)return null;const[presets,setPresets]=React.useState(()=>{const local=localStorage.getItem('daw_ai_prompt_presets');if(!local)return DEFAULT_PRESETS;try{const parsed=JSON.parse(local);const userPresets=parsed.filter(p=>p.is_user_defined);return[...DEFAULT_PRESETS,...userPresets];}catch(_){return DEFAULT_PRESETS;}});const[search,setSearch]=React.useState('');const[filterCategory,setFilterCategory]=React.useState('ALL');const[editingPreset,setEditingPreset]=React.useState(null);// preset object or 'new' // Form states const[formName,setFormName]=React.useState('');const[formKeywords,setFormKeywords]=React.useState('');const[formCategory,setFormCategory]=React.useState('Orchestral / Film Score');const[formBars,setFormBars]=React.useState(8);const[formBpm,setFormBpm]=React.useState(120);const[formScale,setFormScale]=React.useState('C Minor');const[formTemplate,setFormTemplate]=React.useState('');const savePresets=newPresets=>{setPresets(newPresets);localStorage.setItem('daw_ai_prompt_presets',JSON.stringify(newPresets));};const handleEdit=p=>{setEditingPreset(p);setFormName(p.name);setFormKeywords(p.keywords.join(', '));setFormCategory(p.category);setFormBars(p.default_bars);setFormBpm(p.default_bpm);setFormScale(p.default_scale);setFormTemplate(p.system_instruction_template);};const handleNew=()=>{setEditingPreset('new');setFormName('');setFormKeywords('');setFormCategory('Orchestral / Film Score');setFormBars(8);setFormBpm(120);setFormScale('C Minor');setFormTemplate('');};const handleDelete=id=>{const updated=presets.filter(p=>p.id!==id);savePresets(updated);showToast('Đã xóa preset.','info');};const handleSaveForm=e=>{e.preventDefault();if(!formName.trim()||!formTemplate.trim()){showToast('Vui lòng điền đầy đủ tên và mẫu gợi ý.','warning');return;}const keywordsArray=formKeywords.split(',').map(k=>k.trim()).filter(Boolean);const presetObj={id:editingPreset==='new'?'preset_'+Date.now():editingPreset.id,name:formName.trim(),keywords:keywordsArray,category:formCategory,default_bars:parseInt(formBars)||8,default_bpm:parseInt(formBpm)||120,default_scale:formScale,system_instruction_template:formTemplate.trim(),is_user_defined:true,created_at:editingPreset==='new'?new Date().toISOString():editingPreset.created_at};let updated;if(editingPreset==='new'){updated=[...presets,presetObj];}else{updated=presets.map(p=>p.id===presetObj.id?presetObj:p);}savePresets(updated);setEditingPreset(null);showToast('Đã lưu preset thành công!','success');};const categories=['ALL',...new Set(presets.map(p=>p.category))];const filtered=presets.filter(p=>{const matchesSearch=p.name.toLowerCase().includes(search.toLowerCase())||p.keywords.some(k=>k.toLowerCase().includes(search.toLowerCase()));const matchesCategory=filterCategory==='ALL'||p.category===filterCategory;return matchesSearch&&matchesCategory;});return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4 animate-fade-in"},/*#__PURE__*/React.createElement("div",{className:"bg-[#18181b] border border-zinc-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden text-zinc-100"},/*#__PURE__*/React.createElement("div",{className:"p-4 border-b border-zinc-800 flex items-center justify-between shrink-0 bg-[#202024]"},/*#__PURE__*/React.createElement("h2",{className:"text-sm font-bold tracking-wider uppercase text-purple-400 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-4 h-4"}),"AI Prompt Preset Manager"),/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingPreset(null);onClose();},className:"text-zinc-400 hover:text-zinc-200 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto p-4 flex gap-4 min-h-0"},!editingPreset?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col min-w-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 shrink-0"},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Tìm kiếm preset hoặc từ khóa...",value:search,onChange:e=>setSearch(e.target.value),className:"flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"}),/*#__PURE__*/React.createElement("select",{value:filterCategory,onChange:e=>setFilterCategory(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"},categories.map(c=>/*#__PURE__*/React.createElement("option",{key:c,value:c},c==='ALL'?'Tất cả danh mục':c))),/*#__PURE__*/React.createElement("button",{onClick:handleNew,className:"px-3 py-1 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold flex items-center gap-1.5 shadow transition shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3.5 h-3.5"}),"Tạo mới")),/*#__PURE__*/React.createElement("div",{className:"flex-1 border border-zinc-800 rounded bg-[#0f0f12] overflow-y-auto"},filtered.length===0?/*#__PURE__*/React.createElement("div",{className:"p-8 text-center text-zinc-500 text-xs italic"},"Không tìm thấy preset nào."):/*#__PURE__*/React.createElement("table",{className:"w-full text-left text-xs border-collapse"},/*#__PURE__*/React.createElement("thead",{className:"bg-[#1f1f23] text-zinc-400 font-bold border-b border-zinc-800"},/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/4"},"Tên Preset"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/4"},"Từ khóa kích hoạt"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6"},"Số Bar"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6"},"BPM"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6 text-right"},"Hành động"))),/*#__PURE__*/React.createElement("tbody",null,filtered.map(p=>/*#__PURE__*/React.createElement("tr",{key:p.id,className:"border-b border-zinc-800/50 hover:bg-zinc-850"},/*#__PURE__*/React.createElement("td",{className:"p-2.5 font-semibold text-purple-300"},p.name),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400 font-mono text-[11px] truncate max-w-[150px]"},p.keywords.join(', ')),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400"},p.default_bars," Bars"),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400"},p.default_bpm," BPM"),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-right flex items-center justify-end gap-1.5 h-full"},/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>handleEdit(p),className:"px-2 py-0.5 bg-zinc-850 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[10px]"},"Sửa"),/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>handleDelete(p.id),className:"px-2 py-0.5 bg-red-950/40 hover:bg-red-800 text-red-400 rounded border border-red-900 text-[10px]"},"Xóa"))))))))):/*#__PURE__*/React.createElement("form",{onSubmit:handleSaveForm,className:"flex-1 flex flex-col gap-3 min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-zinc-400 shrink-0 border-b border-zinc-800 pb-1"},editingPreset==='new'?"TẠO PRESET MỚI":`SỬA PRESET: ${editingPreset.name}`),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Tên Preset"),/*#__PURE__*/React.createElement("input",{type:"text",value:formName,onChange:e=>setFormName(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Danh mục"),/*#__PURE__*/React.createElement("input",{type:"text",value:formCategory,onChange:e=>setFormCategory(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Từ khóa kích hoạt (ngăn cách bằng dấu phẩy)"),/*#__PURE__*/React.createElement("input",{type:"text",value:formKeywords,onChange:e=>setFormKeywords(e.target.value),placeholder:"Ví dụ: epic orchestra, hoành tráng, nhạc phim epic",className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-3"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Số Bars mặc định"),/*#__PURE__*/React.createElement("input",{type:"number",value:formBars,onChange:e=>setFormBars(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"BPM mặc định"),/*#__PURE__*/React.createElement("input",{type:"number",value:formBpm,onChange:e=>setFormBpm(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Âm giai (Scale) mặc định"),/*#__PURE__*/React.createElement("input",{type:"text",value:formScale,onChange:e=>setFormScale(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 flex-1 min-h-0"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"System Prompt Template / Luật soạn nhạc"),/*#__PURE__*/React.createElement("textarea",{value:formTemplate,onChange:e=>setFormTemplate(e.target.value),rows:6,className:"flex-1 bg-zinc-900 border border-zinc-700 rounded p-2.5 text-xs outline-none focus:border-purple-600 text-zinc-200 font-mono resize-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-end gap-2 shrink-0 pt-2 border-t border-zinc-800"},/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>setEditingPreset(null),className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border border-zinc-700 rounded text-xs transition"},"Quay lại"),/*#__PURE__*/React.createElement("button",{type:"submit",className:"px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold shadow transition"},"Lưu Preset")))),/*#__PURE__*/React.createElement("div",{className:"p-4 border-t border-zinc-800 flex justify-end shrink-0 bg-[#202024]"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingPreset(null);onClose();},className:"px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs font-bold shadow transition"},"Đóng"))));};const PianoRollTabEditor=({st,zoom,bpm,viewportWidth,onClose,onUpdateNotes,onSaveNotes,setSubTabs,onPlayPause,onStop,isPlaying,playPreviewNote,showToast,midiDevices,recordingState,recTempMidiNotes,onRecord,selectedMidiInputId,onMidiInputSelect,activeMidiPitches})=>{const[activeRollTool,setActiveRollTool]=React.useState('select');const[snapVal,setSnapVal]=React.useState('1/16');const[ccMode,setCcMode]=React.useState('velocity');const[rollZoom,setRollZoom]=React.useState(60);// local horizontal zoom factor -const[aiBarStart,setAiBarStart]=React.useState(0);const[aiBarEnd,setAiBarEnd]=React.useState(4);const canvasRef=React.useRef(null);const ccCanvasRef=React.useRef(null);const ccWrapperRef=React.useRef(null);const gridScrollRef=React.useRef(null);const keybedRef=React.useRef(null);const keybedMouseDownRef=React.useRef(false);const rulerScrollRef=React.useRef(null);React.useEffect(()=>{const up=()=>{keybedMouseDownRef.current=false;};window.addEventListener('mouseup',up);return()=>window.removeEventListener('mouseup',up);},[]);const NoteHeight=18;const PITCH_START=0;// C0 (render all 128 keys) +const[aiBarStart,setAiBarStart]=React.useState(0);const[aiBarEnd,setAiBarEnd]=React.useState(4);const canvasRef=React.useRef(null);const ccCanvasRef=React.useRef(null);const ccWrapperRef=React.useRef(null);const gridScrollRef=React.useRef(null);const keybedRef=React.useRef(null);const keybedMouseDownRef=React.useRef(false);const rulerScrollRef=React.useRef(null);React.useEffect(()=>{const up=()=>{keybedMouseDownRef.current=false;};window.addEventListener('mouseup',up);return()=>window.removeEventListener('mouseup',up);},[]);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='a'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();setSelectedNoteIds(notes.map(n=>n.id));}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds]);const NoteHeight=18;const PITCH_START=0;// C0 (render all 128 keys) const KeybedPixelHeight=(128-PITCH_START)*NoteHeight;const pixelsPerBeat=rollZoom;const timeSigNum=4;const noteMaxBeat=(st.notes||[]).reduce((max,n)=>Math.max(max,(n.start_beat||0)+(n.duration_beats||1)),0);const[selectionMarquee,setSelectionMarquee]=React.useState(null);// { startBeat, startPitch, currentBeat, currentPitch } const[draggedNote,setDraggedNote]=React.useState(null);// { mode: 'move'|'resize', idx, startOffsetBeat, originalStart } const[hoveredResizeIdx,setHoveredResizeIdx]=React.useState(-1);const[rollBeats,setRollBeats]=React.useState(Math.max(noteMaxBeat+16,64));const rollBeatsRef=React.useRef(rollBeats);rollBeatsRef.current=rollBeats;const totalBeats=Math.max(rollBeats,noteMaxBeat+16,64);// at least 64 beats (16 bars) for scrolling @@ -155,10 +155,13 @@ setSelectedNoteIds([]);setSelectionMarquee({startBeat:beat,startPitch:pitch,curr if(e.ctrlKey&&e.shiftKey){if(clickedNoteIdx!==-1){pushToUndo(notes);const clickedNote=notes[clickedNoteIdx];const idsToClone=[...new Set([...selectedNoteIds,clickedNote.id])];const clones=notes.filter(n=>idsToClone.includes(n.id)).map(n=>({...JSON.parse(JSON.stringify(n)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)}));setNotes(prev=>[...prev,...clones]);const cloneIds=clones.map(c=>c.id);setSelectedNoteIds(cloneIds);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const cloneOffsets=clones.map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:-1,startOffsetBeat:beat,startOffsetPitch:pitch,selectedNotesOffset:cloneOffsets});}return;}// Hovered resize edge (Alt+resize for scaling) if(hoveredResizeIdx!==-1&&e.altKey){pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const allSelected=[...new Set(selectedNoteIds.length>0?selectedNoteIds:[notes[hoveredResizeIdx].id])];const selectedNotes=notes.filter(n=>allSelected.includes(n.id));const firstStart=Math.min(...selectedNotes.map(n=>n.start_beat));const draggedNote=notes[hoveredResizeIdx];setDraggedNote({mode:'scale',idx:hoveredResizeIdx,originalEnd:draggedNote.start_beat+draggedNote.duration_beats,firstStart:firstStart,selectedNoteIds:allSelected});return;}// Hovered resize edge (normal resize) if(hoveredResizeIdx!==-1){pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'resize',idx:hoveredResizeIdx,originalStart:notes[hoveredResizeIdx].start_beat});return;}if(clickedNoteIdx!==-1){// Click on existing note: drag-move -const clickedNote=notes[clickedNoteIdx];let nextSelectedIds;if(!selectedNoteIds.includes(clickedNote.id)){nextSelectedIds=[clickedNote.id];setSelectedNoteIds(nextSelectedIds);}else{nextSelectedIds=selectedNoteIds;}pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const selectedNotesOffset=notes.filter(n=>nextSelectedIds.includes(n.id)).map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:clickedNoteIdx,startOffsetBeat:beat-clickedNote.start_beat,startOffsetPitch:pitch-clickedNote.pitch,selectedNotesOffset:selectedNotesOffset});}else{// Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches) +const clickedNote=notes[clickedNoteIdx];let nextSelectedIds;if(!selectedNoteIds.includes(clickedNote.id)){nextSelectedIds=[clickedNote.id];setSelectedNoteIds(nextSelectedIds);}else{nextSelectedIds=selectedNoteIds;}pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const selectedNotesOffset=notes.filter(n=>nextSelectedIds.includes(n.id)).map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:clickedNoteIdx,startOffsetBeat:beat-clickedNote.start_beat,startOffsetPitch:pitch,selectedNotesOffset:selectedNotesOffset});}else{// Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches) pushToUndo(notes);const start=getSnapBeat(beat,snapVal);const initialDur=getSnapDuration(snapVal);const noteId='note_'+Date.now()+Math.random().toString(36).substr(2,5);const newNote={id:noteId,pitch:snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch,start_beat:start,duration_beats:initialDur,velocity:brushVelocityRef.current,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds([noteId]);setDraggedNote({mode:'draw',idx:-1,startOffsetBeat:start,startOffsetPitch:snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch,drawNoteId:noteId,drawDuration:initialDur,visitedPitches:snapToScaleRef.current?[snapPitchToScale(pitch,selectedScaleRef.current)]:[pitch],initialBeat:start,initialPitch:snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch});// Play the note with SoundFont -if(window.SonicSF){const ctx=getAudioContext();window.SonicSF.playNote(pitch,0.8,300,ctx.currentTime,undefined,null);}}};const handleGridMouseMove=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat;const pitch=127-Math.floor(y/NoteHeight);if(selectionMarquee){const marquee={...selectionMarquee,currentBeat:beat,currentPitch:pitch};setSelectionMarquee(marquee);const minBeat=Math.min(marquee.startBeat,marquee.currentBeat);const maxBeat=Math.max(marquee.startBeat,marquee.currentBeat);const minPitch=Math.min(marquee.startPitch,marquee.currentPitch);const maxPitch=Math.max(marquee.startPitch,marquee.currentPitch);const insideIds=notes.filter(n=>n.start_beat>=minBeat&&n.start_beat<=maxBeat&&n.pitch>=minPitch&&n.pitch<=maxPitch).map(n=>n.id);setSelectedNoteIds(insideIds);return;}// Right-click drag → erase sweep -const rc=rightClickDragRef.current;if(rc.active&&(Math.abs(e.clientX-rc.startX)>5||Math.abs(e.clientY-rc.startY)>5)){rc.active=false;swallowContextMenuRef.current=true;notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[]});return;}if(!draggedNote){let foundIdx=-1;for(let i=0;i=n.start_beat){foundIdx=i;break;}}}if(foundIdx!==-1){canvas.style.cursor='ew-resize';setHoveredResizeIdx(foundIdx);}else{canvas.style.cursor=activeRollTool==='eraser'?'pointer':'crosshair';setHoveredResizeIdx(-1);}return;}if(draggedNote.mode==='draw'){const rawDur=beat-draggedNote.startOffsetBeat;const newDur=getSnapBeat(Math.max(0.125,rawDur),snapVal);const visited=draggedNote.visitedPitches||[];if(visited.length<=1){setNotes(prev=>prev.map(n=>{if(n.id!==draggedNote.drawNoteId)return n;return{...n,duration_beats:newDur};}));}const snappedPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;if(!visited.includes(snappedPitch)){const newPitches=[...visited,snappedPitch];const totalSpan=Math.max(0.125,beat-draggedNote.initialBeat);const perNoteDur=totalSpan/newPitches.length;const brushIds=draggedNote.brushIds||[];setNotes(prev=>{const cleaned=prev.filter(n=>!brushIds.includes(n.id)&&n.id!==draggedNote.drawNoteId);const brushNotes=newPitches.map((p,i)=>({id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+i,pitch:p,start_beat:draggedNote.initialBeat+i*perNoteDur,duration_beats:Math.max(0.125,perNoteDur*0.9),velocity:brushVelocityRef.current,pan:0.0}));const newBrushIds=brushNotes.map(bn=>bn.id);setSelectedNoteIds(newBrushIds);draggedNote.brushIds=newBrushIds;draggedNote.visitedPitches=newPitches;return[...cleaned,...brushNotes];});}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beatprev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapVal);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapVal);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const firstOffset=draggedNote.selectedNotesOffset&&draggedNote.selectedNotesOffset[0];if(!firstOffset)return;const firstNote=notes.find(n=>n.id===firstOffset.id);if(!firstNote)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapVal)-firstOffset.originalStartBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch)-firstOffset.originalPitch;setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+deltaBeat),snapVal),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const handleCCMouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat;let noteIdx=notes.findIndex(n=>beat>=n.start_beat&&beat<=n.start_beat+n.duration_beats);if(noteIdx===-1){let minDistance=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const dist=Math.abs(center-beat);if(dist{if(idx===-1)return;setNotes(prev=>prev.map((n,i)=>{if(i!==idx)return n;if(ccMode==='pan'){return{...n,pan:(v-0.5)*2.0};}return{...n,velocity:v};}));};if(e.ctrlKey){if(noteIdx!==-1)paintNote(noteIdx,val);ccDragRef.current={active:true,lastBeat:beat,lastPainted:noteIdx!==-1?[noteIdx]:[]};return;}if(noteIdx!==-1)paintNote(noteIdx,val);};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat;const val=Math.max(0,Math.min(1,(h-y)/h));const drag=ccDragRef.current;const painted=drag.lastPainted||[];const candidateIdx=notes.findIndex(n=>beat>=n.start_beat&&beat<=n.start_beat+n.duration_beats);if(candidateIdx!==-1&&!painted.includes(candidateIdx)){setNotes(prev=>prev.map((n,i)=>{if(i!==candidateIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};}));drag.lastPainted=[...painted,candidateIdx];}else if(candidateIdx===-1){let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-beat);if(dprev.map((n,i)=>{if(i!==nearest)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};}));drag.lastPainted=[...painted,nearest];}}};const renderKeybed=()=>{const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;const snapToScaleRef=React.useRef(true);snapToScaleRef.current=st.snapToScale!==undefined?st.snapToScale:true;const[scaleMenuPos,setScaleMenuPos]=React.useState(null);const scaleMenuOriginRef=React.useRef(null);const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);const snapPitchToScale=(pitch,scale)=>{if(!scale)return pitch;const octave=Math.floor(pitch/12);const noteInOctave=pitch%12;if(scale.includes(noteInOctave))return pitch;let best=noteInOctave;let minDist=12;scale.forEach(s=>{const dist=Math.abs(s-noteInOctave);if(dist{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subItems=[];Object.keys(val).forEach(subKey=>{subItems.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:origin.y,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subItems);}}});return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:origin.y,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${bar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"},React.createElement("div",{className:"h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5"}),st.label),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap to Scale"),/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s)),className:`w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale!==undefined?st.snapToScale:true)?'bg-yellow-600':'bg-zinc-700'}`,style:{padding:0}},/*#__PURE__*/React.createElement("div",{className:`w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale!==undefined?st.snapToScale:true)?'translate-x-3.5':'translate-x-0.5'}`}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),/*#__PURE__*/React.createElement("select",{value:snapVal,onChange:e=>setSnapVal(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>/*#__PURE__*/React.createElement("option",{key:v,value:v},v)))),/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),/*#__PURE__*/React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},/*#__PURE__*/React.createElement("option",{value:""},"Input"),/*#__PURE__*/React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>/*#__PURE__*/React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 ml-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:Math.max(0,(st.currentTime||0)-beatSec*4)}:s)),className:"w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",title:"Back 1 bar"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-back",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:onPlayPause,className:`w-6 h-6 flex items-center justify-center rounded border ${isPlaying?'bg-emerald-600 text-black':'bg-cyan-600 text-white'} border-cyan-500`,title:isPlaying?"Pause":"Play"},/*#__PURE__*/React.createElement("i",{"data-lucide":isPlaying?"pause":"play",className:"w-3 h-3 fill-current"})),/*#__PURE__*/React.createElement("button",{onClick:onStop,className:"w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",title:"Stop"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3 h-3 fill-current"})),/*#__PURE__*/React.createElement("button",{onClick:onRecord,className:`w-6 h-6 flex items-center justify-center rounded border ${recordingState==='RECORDING'?'bg-red-600 text-white border-red-500 animate-pulse':'bg-zinc-800 text-red-500 border-zinc-700'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:"w-3 h-3 fill-current"})),/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:(st.currentTime||0)+beatSec*4}:s)),className:"w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",title:"Forward 1 bar"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-forward",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500"},"AI:"),/*#__PURE__*/React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),/*#__PURE__*/React.createElement("span",{className:"text-zinc-500"},"-"),/*#__PURE__*/React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),/*#__PURE__*/React.createElement("span",{className:"text-zinc-500"},"bar")),/*#__PURE__*/React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['velocity','pan'].map(mode=>/*#__PURE__*/React.createElement("button",{key:mode,onClick:()=>setCcMode(mode),className:`px-2.5 py-1 rounded capitalize ${ccMode===mode?'bg-purple-900/60 text-purple-300 font-bold border border-purple-700':'text-zinc-400 hover:text-zinc-200'}`},mode)))),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='pan'?'Pan':'Vel'),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"Lưu"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"))),/*#__PURE__*/React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),/*#__PURE__*/React.createElement("div",{ref:rulerScrollRef,className:"flex-1 overflow-hidden",onMouseDown:e=>{const rect=e.currentTarget.getBoundingClientRect();const x=e.clientX-rect.left+e.currentTarget.scrollLeft;const clickBeat=x/pixelsPerBeat;const clickTime=clickBeat*beatSec;if(clickTime>=0){setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}},/*#__PURE__*/React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels()))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/*#__PURE__*/React.createElement("div",{ref:keybedRef,onScroll:handleKeybedScroll,className:"w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",style:{scrollbarWidth:'none',msOverflowStyle:'none'}},renderKeybed()),/*#__PURE__*/React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${viewWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"})))),showCC&&/*#__PURE__*/React.createElement("div",{style:{height:`${ccHeight}px`},className:"bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"},/*#__PURE__*/React.createElement("div",{onMouseDown:e=>{e.preventDefault();const startY=e.clientY;const startH=ccHeight;const onMove=ev=>{setCcHeight(Math.max(40,startH+startY-ev.clientY));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);},className:"absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"}),/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"},ccMode.toUpperCase()),/*#__PURE__*/React.createElement("div",{ref:ccWrapperRef,className:"flex-1 overflow-x-hidden min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,onMouseMove:handleCCMouseMove,onMouseUp:()=>{ccDragRef.current=null;},onMouseLeave:()=>{ccDragRef.current=null;},className:"absolute inset-0"})))),scaleMenuPos&&renderScaleContextMenu());};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];if(trackType==="AUDIO"&&t.clips){t.clips.forEach(c=>{const durationSec=c.buffer?c.buffer.duration:4.0;items.push({id:c.id,name:c.name||"Audio Clip",type:"AUDIO_ITEM",start_bar:c.startTime/secondsPerBar,duration_bars:durationSec/(c.speed||1.0)/secondsPerBar,clip_start_offset_bars:0.0,source_data:{audio_file_url:t.serverFileId?`/static/audio/uploads/${t.serverFileId}`:"",sample_rate:c.buffer?c.buffer.sampleRate:44100,channels:c.buffer?c.buffer.numberOfChannels:2,gain:1.0}});});}else if(trackType==="MIDI"&&t.midiItems){t.midiItems.forEach(m=>{items.push({id:m.id,name:m.name||"MIDI Item",type:"MIDI_ITEM",start_bar:m.startTime/secondsPerBar,duration_bars:m.duration?m.duration/secondsPerBar:4.0,clip_start_offset_bars:0.0,source_data:{total_buffer_bars:m.duration?m.duration/secondsPerBar:8.0,notes:(m.notes||[]).map(n=>({id:n.id||'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))}});});}else if(trackType==="SECTION"&&t.sections){t.sections.forEach(s=>{items.push({id:s.id,name:s.name||"Section Item",type:"SECTION_ITEM",start_bar:s.start/secondsPerBar,duration_bars:s.duration/secondsPerBar,clip_start_offset_bars:0.0,source_data:{referenced_section_id:s.sectionId||s.id}});});}return{id:t.id,name:t.name,type:trackType,volume_db:t.volumeDb||0.0,pan:t.pan||0.0,mute:t.muted||false,solo:t.solo||false,instrument_id:t.instrumentId||null,instrument_program:t.instrumentProgram!==undefined?t.instrumentProgram:null,instrument_name:t.instrumentName||null,items:items};});};const deserializeTracksList=(schemaTracks,secondsPerBar,sectionStore)=>{return(schemaTracks||[]).map(t=>{const clips=[];const sections=[];const midiItems=[];(t.items||[]).forEach(item=>{if(item.type==="AUDIO_ITEM"){const src=item.source_data||{};clips.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,speed:1.0});}else if(item.type==="MIDI_ITEM"){const src=item.source_data||{};midiItems.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,notes:(src.notes||[]).map(n=>({id:n.id,pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))});}else if(item.type==="SECTION_ITEM"){const src=item.source_data||{};const secId=src.referenced_section_id;const secContainer=sectionStore?sectionStore[secId]:null;sections.push({id:item.id,name:item.name,start:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,sectionId:secId,tracks:secContainer?deserializeTracksList(secContainer.tracks,secondsPerBar,sectionStore):null});}});return{id:t.id,name:t.name,volumeDb:t.volume_db||0.0,pan:t.pan||0.0,muted:t.mute||false,solo:t.solo||false,color:t.id==='1'?'#0f766e':'#1d4ed8',markers:[],serverFileId:t.items&&t.items.find(i=>i.type==="AUDIO_ITEM")?.source_data?.audio_file_url?.split("/").pop()||null,clips:clips,sections:sections,midiItems:midiItems,instrumentId:t.instrument_id||null,instrumentProgram:t.instrument_program!==null?t.instrument_program:undefined,instrumentName:t.instrument_name||null};});};const serializeProjectToSchema=(projectId,name,bpmVal,tracksList,subTabsList,sessionTabsList)=>{const secondsPerBar=60.0/parseFloat(bpmVal||120)*4;const mainTracks=serializeTracksList(tracksList,secondsPerBar);const sectionStore={};// Helper: compute length_bars from tracks content +if(window.SonicSF){const ctx=getAudioContext();window.SonicSF.playNote(pitch,0.8,300,ctx.currentTime,undefined,null);}}};const handleGridMouseMove=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat;const pitch=127-Math.floor(y/NoteHeight);if(selectionMarquee){const marquee={...selectionMarquee,currentBeat:beat,currentPitch:pitch};setSelectionMarquee(marquee);const minBeat=Math.min(marquee.startBeat,marquee.currentBeat);const maxBeat=Math.max(marquee.startBeat,marquee.currentBeat);const minPitch=Math.min(marquee.startPitch,marquee.currentPitch);const maxPitch=Math.max(marquee.startPitch,marquee.currentPitch);const insideIds=notes.filter(n=>{const withinPitch=n.pitch>=minPitch&&n.pitch<=maxPitch;if(!withinPitch)return false;const noteEnd=n.start_beat+n.duration_beats;if(marquee.startBeat<=marquee.currentBeat){// Left to right: select if any overlap +return n.start_beat<=maxBeat&¬eEnd>=minBeat;}else{// Right to left: select only if fully covered +return n.start_beat>=minBeat&¬eEnd<=maxBeat;}}).map(n=>n.id);setSelectedNoteIds(insideIds);return;}// Right-click drag → erase sweep +const rc=rightClickDragRef.current;if(rc.active&&(Math.abs(e.clientX-rc.startX)>5||Math.abs(e.clientY-rc.startY)>5)){rc.active=false;swallowContextMenuRef.current=true;notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[]});return;}if(!draggedNote){let foundIdx=-1;for(let i=0;i=n.start_beat){foundIdx=i;break;}}}if(foundIdx!==-1){canvas.style.cursor='ew-resize';setHoveredResizeIdx(foundIdx);}else{canvas.style.cursor=activeRollTool==='eraser'?'pointer':'crosshair';setHoveredResizeIdx(-1);}return;}if(draggedNote.mode==='draw'){const rawDur=beat-draggedNote.startOffsetBeat;const newDur=getSnapBeat(Math.max(0.125,rawDur),snapVal);const visited=draggedNote.visitedPitches||[];if(visited.length<=1){setNotes(prev=>prev.map(n=>{if(n.id!==draggedNote.drawNoteId)return n;return{...n,duration_beats:newDur};}));}const snappedPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;if(!visited.includes(snappedPitch)){const newPitches=[...visited,snappedPitch];const totalSpan=Math.max(0.125,beat-draggedNote.initialBeat);const perNoteDur=totalSpan/newPitches.length;const brushIds=draggedNote.brushIds||[];setNotes(prev=>{const cleaned=prev.filter(n=>!brushIds.includes(n.id)&&n.id!==draggedNote.drawNoteId);const brushNotes=newPitches.map((p,i)=>({id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+i,pitch:p,start_beat:draggedNote.initialBeat+i*perNoteDur,duration_beats:Math.max(0.125,perNoteDur*0.9),velocity:brushVelocityRef.current,pan:0.0}));const newBrushIds=brushNotes.map(bn=>bn.id);setSelectedNoteIds(newBrushIds);draggedNote.brushIds=newBrushIds;draggedNote.visitedPitches=newPitches;return[...cleaned,...brushNotes];});}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beatprev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapVal);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapVal);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const firstOffset=draggedNote.selectedNotesOffset&&draggedNote.selectedNotesOffset[0];if(!firstOffset)return;const firstNote=notes.find(n=>n.id===firstOffset.id);if(!firstNote)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapVal)-firstOffset.originalStartBeat;// Clamp so no note goes past beat 0 +const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+clampedDeltaBeat),snapVal),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const handleCCMouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat;let noteIdx=notes.findIndex(n=>beat>=n.start_beat&&beat<=n.start_beat+n.duration_beats);if(noteIdx===-1){let minDistance=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const dist=Math.abs(center-beat);if(dist{if(idx===-1)return;setNotes(prev=>prev.map((n,i)=>{if(i!==idx)return n;if(ccMode==='pan'){return{...n,pan:(v-0.5)*2.0};}return{...n,velocity:v};}));};if(e.ctrlKey){if(noteIdx!==-1)paintNote(noteIdx,val);ccDragRef.current={active:true,lastBeat:beat,lastPainted:noteIdx!==-1?[noteIdx]:[]};return;}if(noteIdx!==-1)paintNote(noteIdx,val);};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat;const val=Math.max(0,Math.min(1,(h-y)/h));const drag=ccDragRef.current;const painted=drag.lastPainted||[];const candidateIdx=notes.findIndex(n=>beat>=n.start_beat&&beat<=n.start_beat+n.duration_beats);if(candidateIdx!==-1&&!painted.includes(candidateIdx)){setNotes(prev=>prev.map((n,i)=>{if(i!==candidateIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};}));drag.lastPainted=[...painted,candidateIdx];}else if(candidateIdx===-1){let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-beat);if(dprev.map((n,i)=>{if(i!==nearest)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};}));drag.lastPainted=[...painted,nearest];}}};const renderKeybed=()=>{const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;const snapToScaleRef=React.useRef(true);snapToScaleRef.current=st.snapToScale!==undefined?st.snapToScale:true;const[scaleMenuPos,setScaleMenuPos]=React.useState(null);const scaleMenuOriginRef=React.useRef(null);const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);const snapPitchToScale=(pitch,scale)=>{if(!scale)return pitch;const octave=Math.floor(pitch/12);const noteInOctave=pitch%12;if(scale.includes(noteInOctave))return pitch;let best=noteInOctave;let minDist=12;scale.forEach(s=>{const dist=Math.abs(s-noteInOctave);if(dist{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subItems=[];Object.keys(val).forEach(subKey=>{subItems.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:origin.y,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subItems);}}});return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:origin.y,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${bar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"},React.createElement("div",{className:"h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5"}),st.label),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap to Scale"),/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s)),className:`w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale!==undefined?st.snapToScale:true)?'bg-yellow-600':'bg-zinc-700'}`,style:{padding:0}},/*#__PURE__*/React.createElement("div",{className:`w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale!==undefined?st.snapToScale:true)?'translate-x-3.5':'translate-x-0.5'}`}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),/*#__PURE__*/React.createElement("select",{value:snapVal,onChange:e=>setSnapVal(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>/*#__PURE__*/React.createElement("option",{key:v,value:v},v)))),/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),/*#__PURE__*/React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},/*#__PURE__*/React.createElement("option",{value:""},"Input"),/*#__PURE__*/React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>/*#__PURE__*/React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 ml-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:Math.max(0,(st.currentTime||0)-beatSec*4)}:s)),className:"w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",title:"Back 1 bar"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-back",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:onPlayPause,className:`w-6 h-6 flex items-center justify-center rounded border ${isPlaying?'bg-emerald-600 text-black':'bg-cyan-600 text-white'} border-cyan-500`,title:isPlaying?"Pause":"Play"},/*#__PURE__*/React.createElement("i",{"data-lucide":isPlaying?"pause":"play",className:"w-3 h-3 fill-current"})),/*#__PURE__*/React.createElement("button",{onClick:onStop,className:"w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",title:"Stop"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3 h-3 fill-current"})),/*#__PURE__*/React.createElement("button",{onClick:onRecord,className:`w-6 h-6 flex items-center justify-center rounded border ${recordingState==='RECORDING'?'bg-red-600 text-white border-red-500 animate-pulse':'bg-zinc-800 text-red-500 border-zinc-700'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:"w-3 h-3 fill-current"})),/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:(st.currentTime||0)+beatSec*4}:s)),className:"w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",title:"Forward 1 bar"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-forward",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500"},"AI:"),/*#__PURE__*/React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),/*#__PURE__*/React.createElement("span",{className:"text-zinc-500"},"-"),/*#__PURE__*/React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),/*#__PURE__*/React.createElement("span",{className:"text-zinc-500"},"bar")),/*#__PURE__*/React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['velocity','pan'].map(mode=>/*#__PURE__*/React.createElement("button",{key:mode,onClick:()=>setCcMode(mode),className:`px-2.5 py-1 rounded capitalize ${ccMode===mode?'bg-purple-900/60 text-purple-300 font-bold border border-purple-700':'text-zinc-400 hover:text-zinc-200'}`},mode)))),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='pan'?'Pan':'Vel'),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"Lưu"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"))),/*#__PURE__*/React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),/*#__PURE__*/React.createElement("div",{ref:rulerScrollRef,className:"flex-1 overflow-hidden",onMouseDown:e=>{const rect=e.currentTarget.getBoundingClientRect();const x=e.clientX-rect.left+e.currentTarget.scrollLeft;const clickBeat=x/pixelsPerBeat;const clickTime=clickBeat*beatSec;if(clickTime>=0){setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}},/*#__PURE__*/React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels()))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/*#__PURE__*/React.createElement("div",{ref:keybedRef,onScroll:handleKeybedScroll,className:"w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",style:{scrollbarWidth:'none',msOverflowStyle:'none'}},renderKeybed()),/*#__PURE__*/React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${viewWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"})))),showCC&&/*#__PURE__*/React.createElement("div",{style:{height:`${ccHeight}px`},className:"bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"},/*#__PURE__*/React.createElement("div",{onMouseDown:e=>{e.preventDefault();const startY=e.clientY;const startH=ccHeight;const onMove=ev=>{setCcHeight(Math.max(40,startH+startY-ev.clientY));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);},className:"absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"}),/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"},ccMode.toUpperCase()),/*#__PURE__*/React.createElement("div",{ref:ccWrapperRef,className:"flex-1 overflow-x-hidden min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,onMouseMove:handleCCMouseMove,onMouseUp:()=>{ccDragRef.current=null;},onMouseLeave:()=>{ccDragRef.current=null;},className:"absolute inset-0"})))),scaleMenuPos&&renderScaleContextMenu());};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];if(trackType==="AUDIO"&&t.clips){t.clips.forEach(c=>{const durationSec=c.buffer?c.buffer.duration:4.0;items.push({id:c.id,name:c.name||"Audio Clip",type:"AUDIO_ITEM",start_bar:c.startTime/secondsPerBar,duration_bars:durationSec/(c.speed||1.0)/secondsPerBar,clip_start_offset_bars:0.0,source_data:{audio_file_url:t.serverFileId?`/static/audio/uploads/${t.serverFileId}`:"",sample_rate:c.buffer?c.buffer.sampleRate:44100,channels:c.buffer?c.buffer.numberOfChannels:2,gain:1.0}});});}else if(trackType==="MIDI"&&t.midiItems){t.midiItems.forEach(m=>{items.push({id:m.id,name:m.name||"MIDI Item",type:"MIDI_ITEM",start_bar:m.startTime/secondsPerBar,duration_bars:m.duration?m.duration/secondsPerBar:4.0,clip_start_offset_bars:0.0,source_data:{total_buffer_bars:m.duration?m.duration/secondsPerBar:8.0,notes:(m.notes||[]).map(n=>({id:n.id||'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))}});});}else if(trackType==="SECTION"&&t.sections){t.sections.forEach(s=>{items.push({id:s.id,name:s.name||"Section Item",type:"SECTION_ITEM",start_bar:s.start/secondsPerBar,duration_bars:s.duration/secondsPerBar,clip_start_offset_bars:0.0,source_data:{referenced_section_id:s.sectionId||s.id}});});}return{id:t.id,name:t.name,type:trackType,volume_db:t.volumeDb||0.0,pan:t.pan||0.0,mute:t.muted||false,solo:t.solo||false,instrument_id:t.instrumentId||null,instrument_program:t.instrumentProgram!==undefined?t.instrumentProgram:null,instrument_name:t.instrumentName||null,items:items};});};const deserializeTracksList=(schemaTracks,secondsPerBar,sectionStore)=>{return(schemaTracks||[]).map(t=>{const clips=[];const sections=[];const midiItems=[];(t.items||[]).forEach(item=>{if(item.type==="AUDIO_ITEM"){const src=item.source_data||{};clips.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,speed:1.0});}else if(item.type==="MIDI_ITEM"){const src=item.source_data||{};midiItems.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,notes:(src.notes||[]).map(n=>({id:n.id,pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))});}else if(item.type==="SECTION_ITEM"){const src=item.source_data||{};const secId=src.referenced_section_id;const secContainer=sectionStore?sectionStore[secId]:null;sections.push({id:item.id,name:item.name,start:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,sectionId:secId,tracks:secContainer?deserializeTracksList(secContainer.tracks,secondsPerBar,sectionStore):null});}});return{id:t.id,name:t.name,volumeDb:t.volume_db||0.0,pan:t.pan||0.0,muted:t.mute||false,solo:t.solo||false,color:t.id==='1'?'#0f766e':'#1d4ed8',markers:[],serverFileId:t.items&&t.items.find(i=>i.type==="AUDIO_ITEM")?.source_data?.audio_file_url?.split("/").pop()||null,clips:clips,sections:sections,midiItems:midiItems,instrumentId:t.instrument_id||null,instrumentProgram:t.instrument_program!==null?t.instrument_program:undefined,instrumentName:t.instrument_name||null};});};const serializeProjectToSchema=(projectId,name,bpmVal,tracksList,subTabsList,sessionTabsList)=>{const secondsPerBar=60.0/parseFloat(bpmVal||120)*4;const mainTracks=serializeTracksList(tracksList,secondsPerBar);const sectionStore={};// Helper: compute length_bars from tracks content const computeLengthBars=(tracksArr,spb)=>{let maxSec=0;(tracksArr||[]).forEach(tr=>{(tr.clips||[]).forEach(c=>{const end=(c.startTime||0)+(c.buffer?c.buffer.duration/(c.speed||1.0):4);if(end>maxSec)maxSec=end;});(tr.midiItems||[]).forEach(m=>{const end=(m.startTime||0)+(m.duration||4);if(end>maxSec)maxSec=end;});});return Math.ceil((maxSec||4)/spb);};// 1. Populate from sessionTabsList (open tabs) (sessionTabsList||[]).forEach(st=>{const serializedTracks=serializeTracksList(st.tracks,secondsPerBar);sectionStore[st.sectionId]={id:st.sectionId,name:st.name,is_root:false,length_bars:computeLengthBars(st.tracks,secondsPerBar),auto_compute_length:true,tracks:serializedTracks,color:st.color||null};});// 2. Also populate from tracksList (closed tabs saved inside Section items) const scanForSections=tracks=>{(tracks||[]).forEach(t=>{if(t.sections){t.sections.forEach(s=>{const secId=s.sectionId||s.id;if(s.tracks&&!sectionStore[secId]){sectionStore[secId]={id:secId,name:s.name,is_root:false,length_bars:computeLengthBars(s.tracks,secondsPerBar),auto_compute_length:true,tracks:serializeTracksList(s.tracks,secondsPerBar),color:s.color||null};}if(s.tracks){scanForSections(s.tracks);}});}});};scanForSections(tracksList);const subTabs=(subTabsList||[]).map(st=>{return{id:st.id,label:st.label,type:st.type,track_id:st.trackId,target_id:st.target_id,parent_tab_id:st.parent_tab_id,notes:st.notes||[],duration:st.duration||4,instrument_program:st.instrumentProgram,instrument_name:st.instrumentName,current_time:st.currentTime||0,color:st.color||null};});return{project_id:projectId||'proj_'+Date.now(),metadata:{title:name||"Dự án mới",bpm:parseFloat(bpmVal||120),time_signature_numerator:4,time_signature_denominator:4,sample_rate:44100},main_session:{id:"main",name:"MAIN SESSION",is_root:true,length_bars:(()=>{let maxBar=16.0;(mainTracks||[]).forEach(t=>{(t.items||[]).forEach(item=>{const end=(item.start_bar||0)+(item.duration_bars||4);if(end>maxBar)maxBar=end;});});return maxBar;})(),auto_compute_length:true,tracks:mainTracks},sub_tabs:subTabs,section_store:sectionStore};};const deserializeProjectFromSchema=schemaObj=>{const bpmVal=schemaObj.metadata?schemaObj.metadata.bpm:120;const secondsPerBar=60.0/bpmVal*4;const sectionStore=schemaObj.section_store||{};const restoredTracks=deserializeTracksList(schemaObj.main_session.tracks,secondsPerBar,sectionStore);const restoredSessionTabs=[];Object.keys(sectionStore).forEach(secId=>{const secContainer=sectionStore[secId];const secTracks=deserializeTracksList(secContainer.tracks,secondsPerBar,sectionStore);restoredSessionTabs.push({id:'session_'+secId,name:secContainer.name,sectionId:secId,tracks:secTracks,length_bars:secContainer.length_bars||16.0,auto_compute_length:secContainer.auto_compute_length!==undefined?secContainer.auto_compute_length:true,color:secContainer.color||null});});const restoredSubTabs=(schemaObj.sub_tabs||[]).map(st=>{return{id:st.id,label:st.label,type:st.type,trackId:st.track_id,target_id:st.target_id,parent_tab_id:st.parent_tab_id,notes:st.notes||[],duration:st.duration||4,instrumentProgram:st.instrument_program,instrumentName:st.instrument_name,currentTime:st.current_time||0,color:st.color||null};});return{bpm:bpmVal,tracks:restoredTracks,sessionTabs:restoredSessionTabs,subTabs:restoredSubTabs};};const App=()=>{// ── State Definitions ── @@ -301,7 +304,7 @@ useEffect(()=>{const handleMouseMove=e=>{const stretch=stretchedClipRef.current; const handleSectionItemDragStart=(trackId,itemType,itemId,clickOffset)=>{setDraggedSectionItem({trackId,itemType,itemId,clickOffset});};// ── Section / MIDI Item Resize Start ── const handleSectionItemResizeStart=(trackId,itemType,itemId,side,clickTime)=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===trackId);if(!track)return;const items=itemType==='section'?track.sections:track.midiItems;const item=(items||[]).find(it=>it.id===itemId);if(!item)return;const start=itemType==='section'?item.start:item.startTime;setResizedSectionItem({trackId,itemType,itemId,side,originalStart:start,originalDuration:item.duration});};// ── Document-level mousemove/mouseup for Section/MIDI item drag ── useEffect(()=>{const handleMouseMove=e=>{const drag=draggedSectionItemRef.current;if(!drag)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);const beatSec=60.0/(parseInt(bpm)||120);const secondsPerBar=beatSec*4;const marginBar=maxDurationRef.current-secondsPerBar;const rawStart=Math.max(0,Math.min(time-drag.clickOffset,marginBar));const newStart=snapTime(rawStart,snapValueRef.current,bpmRef.current);const itemPx=newStart*zoom;const keepMargin=80;if(itemPx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=itemPx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(itemPxn+1);}const targetTrackId=hoveredTrackIdRef.current||drag.trackId;updateActiveTracks(prev=>{let movedItem=null;for(let track of prev){const items=drag.itemType==='section'?track.sections||[]:track.midiItems||[];const found=items.find(it=>it.id===drag.itemId);if(found){movedItem=found;break;}}return prev.map(t=>{const items=drag.itemType==='section'?t.sections||[]:t.midiItems||[];const updatedItems=items.filter(it=>it.id!==drag.itemId);if(t.id===targetTrackId){if(movedItem){updatedItems.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}else{if(drag.itemType==='section'){updatedItems.push({id:drag.itemId,name:'Section',start:newStart,duration:4*secondsPerBar,color:'#06b6d4'});}else{updatedItems.push({id:drag.itemId,name:'MIDI Item',startTime:newStart,duration:4*secondsPerBar,notes:[],color:'#a78bfa'});}}}return drag.itemType==='section'?{...t,sections:updatedItems}:{...t,midiItems:updatedItems};});});if(drag.trackId!==targetTrackId){setDraggedSectionItem(prev=>({...prev,trackId:targetTrackId}));}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;setDraggedSectionItem(null);showToast(`Đã di chuyển ${drag.itemType==='section'?'section':'MIDI item'}.`,'success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Document-level mousemove/mouseup for Section/MIDI item resize ── -useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const newDuration=Math.max(0.1,clampedTime-resize.originalStart);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePxn+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);const handleSelectRange=(start,end,reset)=>{const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}// LOOP_EDITOR_2.md §4.2: new selection = enable looping +useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const snappedDuration=snapValueRef.current!=='free'?snapTime(clampedTime-resize.originalStart,snapValueRef.current,bpm):clampedTime-resize.originalStart;const newDuration=Math.max(0.1,snappedDuration);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePxn+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);const handleSelectRange=(start,end,reset)=>{const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}// LOOP_EDITOR_2.md §4.2: new selection = enable looping setSelectionCleared(false);};const handleSelectionInputChange=(field,val)=>{const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){// Editing local selection directly if(field==='start'){setLocalSelectionStart(numericVal);}else{setLocalSelectionEnd(numericVal);}}else{if(field==='start'){setSelectionStart(numericVal);}else{setSelectionEnd(numericVal);}}};const selectionStats=useMemo(()=>{if(selLeft===null||selRight===null){return{start:0,end:0,length:0};}const s=Math.min(selLeft,selRight);const e=Math.max(selLeft,selRight);return{start:parseFloat(s.toFixed(3)),end:parseFloat(e.toFixed(3)),length:parseFloat((e-s).toFixed(3))};},[selLeft,selRight]);// ── Handle Drag (selection resize) ── const handleHandleDragStart=(e,side)=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;if(side==='left'){const newLeft=Math.max(0,Math.min(initialRight-0.05,initialLeft+deltaSec));setStart(newLeft);setEnd(initialRight);}else{const newRight=Math.max(initialLeft+0.05,Math.min(maxDuration,initialRight+deltaSec));setStart(initialLeft);setEnd(newRight);}};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};const handleSelectionBodyDragStart=e=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const widthSec=initialRight-initialLeft;const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;let newLeft=initialLeft+deltaSec;let newRight=initialRight+deltaSec;if(newLeft<0){newLeft=0;newRight=widthSec;}if(newRight>maxDuration){newRight=maxDuration;newLeft=maxDuration-widthSec;}setStart(newLeft);setEnd(newRight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Track Controls ──