diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 7afc66d..111015f 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -472,11 +472,15 @@ class PreviewRequest(BaseModel): @router.post("/open-in-carla") async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(get_current_user)): - """Mở Carla (native GUI host) trên máy hiện tại — Windows desktop mode. + """Mở Carla với VSTi đã chọn — TỰ ĐỘNG load plugin (native GUI + keyboard). - Carla là app ngoài do user tự cài (GPL-2.0+ → không bundle/nhúng). App chỉ - spawn tiến trình; user chỉnh preset trong GUI rồi Save → .vstpreset → upload - vào thư viện preset → gán vào track → render engine tải preset tương ứng.""" + Cơ chế: sinh file project .carxs (định dạng XML chính thức của Carla — + `carla.exe [FILE]` nhận project file) chứa node VST3 + ... → Carla mở lên là plugin đã load sẵn, + kèm on-screen MIDI keyboard (PixmapKeyboard) để preview realtime. + + Carla là app ngoài do user tự giải nén (GPL-2.0+ → không bundle/nhúng); + app chỉ spawn tiến trình + trao đổi file preset.""" enforce_password_changed(current_user) from app.core.runtime import find_carla carla = find_carla() @@ -497,15 +501,12 @@ async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(ge plugin_path = plugins[req.plugin_name] except Exception: plugin_path = "" - cmd = [carla] + carxs_path = "" if plugin_path: - # carla-single: mở thẳng 1 plugin thành app standalone có native GUI - single = os.path.join( - os.path.dirname(carla), - "carla-single" + (".exe" if os.name == "nt" else ""), - ) - if os.path.isfile(single): - cmd = [single, plugin_path] + carxs_path = _write_carla_project(req.plugin_name or os.path.basename(plugin_path), plugin_path) + cmd = [carla] + if carxs_path: + cmd.append(carxs_path) try: cwd = os.path.dirname(carla) or None subprocess.Popen(cmd, cwd=cwd, close_fds=os.name != "nt") @@ -514,12 +515,83 @@ async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(ge "started": True, "carla_path": carla, "plugin_path": plugin_path, + "project_file": carxs_path, "cmd": cmd, } except Exception as e: raise HTTPException(status_code=500, detail=f"Không mở được Carla: {e}") +def _write_carla_project(plugin_name: str, plugin_path: str) -> str: + """Sinh file project .carxs cho Carla load sẵn VSTi (chỉ VST3 — VST2 cần + uniqueID không đoán được → mở Carla trống để user tự Add Plugin). + + Định dạng theo source Carla (CarlaEngine::saveProjectInternal + + CarlaStateSave::dumpToMemoryStream): root , + VST3path + Yes1 + 0x0.""" + if not plugin_path or not os.path.exists(plugin_path): + return "" + low = plugin_path.lower() + is_vst3 = low.endswith(".vst3") or low.endswith(".vst3/") or "\\" in plugin_path and plugin_path.rstrip("\\/").lower().endswith(".vst3") + if not is_vst3: + # VST2 (.dll/.so ngoài .vst3): không auto-load được tin cậy → Carla trống + return "" + from xml.sax.saxutils import escape + name = escape(plugin_name or os.path.splitext(os.path.basename(plugin_path))[0]) + binary = escape(plugin_path) + xml = ( + "\n" + "\n" + f"\n" + " \n" + " false\n" + " false\n" + " false\n" + " false\n" + " 100\n" + " 10000\n" + " \n" + " \n" + " \n" + f" VST3\n" + f" {name}\n" + f" {binary}\n" + f" \n" + " \n" + " \n" + " Yes\n" + " 1\n" + " 0x0\n" + " \n" + " \n" + "\n" + ) + try: + proj_dir = os.path.join(settings.STORAGE_DIR, "carla_projects") + os.makedirs(proj_dir, exist_ok=True) + # Dọn project cũ (quá 1 ngày) — tránh rác + try: + now = _time.time() + for f in os.listdir(proj_dir): + fp = os.path.join(proj_dir, f) + try: + if os.path.isfile(fp) and now - os.path.getmtime(fp) > 86400: + os.remove(fp) + except Exception: + pass + except Exception: + pass + safe = "".join(c for c in plugin_name if c.isalnum() or c in " _-")[:40].strip() or "plugin" + carxs = os.path.join(proj_dir, f"{safe}_{uuid.uuid4().hex[:8]}.carxs") + with open(carxs, "w", encoding="utf-8") as fh: + fh.write(xml) + return carxs + except Exception: + return "" + + @router.post("/preview") async def preview_instrument(req: PreviewRequest): """Quick-render preview VSTi (âm thật, cùng code path với export).""" diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 45973cb..314e881 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -5294,6 +5294,8 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument } const [pmSfLoading, setPmSfLoading] = React.useState({}); // Force re-render sau khi định vị Carla (capabilities đổi) const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0); + // Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker) + const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState(''); React.useEffect(() => { if (isOpen) { window.SonicAPI.listPlugins() @@ -5416,6 +5418,24 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument } } } catch (err) { showToast('Lỗi định vị Carla: ' + (err.message || err), 'error'); } }; + // Khai báo thư mục Carla trực tiếp (nhập tay) — tương tự Định vị nhưng + // không cần hộp thoại chọn thư mục. + const saveCarlaInput = async () => { + const p = (pmCarlaPathInput || '').trim(); + if (!p) { showToast('Nhập đường dẫn thư mục chứa carla.exe', 'warning'); return; } + try { + const r = await window.SonicAPI.setCarlaPath(p); + if (r && r.success && r.carla_path) { + window.SonicRuntime.capabilities = r; + document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0'; + setPmCarlaVersion(v => v + 1); + setPmCarlaPathInput(''); + showToast('Đã lưu Carla: ' + r.carla_path, 'success'); + } else { + showToast('Không tìm thấy carla.exe trong đường dẫn đã nhập', 'error'); + } + } catch (err) { showToast('Lỗi: ' + (err.message || err), 'error'); } + }; // Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. const saveAndScanDirs = async () => { setPmScanning(true); @@ -5667,6 +5687,20 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument } onClick: () => { window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) showToast('Đã mở Carla', 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); }, className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1' }, React.createElement('i', { 'data-lucide': 'play', className: 'w-3 h-3' }), 'Mở Carla') + ), + React.createElement('div', { className: 'flex gap-2 mt-2' }, + React.createElement('input', { + type: 'text', + placeholder: 'Hoặc nhập thư mục chứa carla.exe (VD: D:/Tools/Carla)', + value: pmCarlaPathInput, + onChange: e => setPmCarlaPathInput(e.target.value), + onKeyDown: e => { if (e.key === 'Enter') saveCarlaInput(); }, + className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-teal-600' + }), + React.createElement('button', { + onClick: saveCarlaInput, + className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition shrink-0' + }, 'Lưu') ) ), // Plugin directories section (folder picker + save + scan) @@ -30099,7 +30133,18 @@ STRICT CONSTRAINTS: className: "flex items-stretch" }, /*#__PURE__*/React.createElement("button", { - onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); }, + onClick: () => { + setInstrumentDropdownTrackId(null); + setInstrumentDropdownBtnRect(null); + setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); + // TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) — + // Carla load sẵn plugin, native GUI + keyboard ảo để preview realtime. + if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) { + window.SonicAPI.openInCarla(v.id).then(function (r) { + if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success'); + }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); + } + }, className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between" }, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")), window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 072b688..f2752d1 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -365,7 +365,8 @@ useEffect(()=>{if(!isOpen)return;if(window.SonicAPI&&window.SonicAPI.apiRequest) .catch(function(){setFirstTime(false);});}else{setFirstTime(false);}},[isOpen]);const handleSubmit=async e=>{e.preventDefault();setError('');setLoading(true);try{if(activeTab==='login'){const targetUsername=username.trim()||'admin';const targetPwd=password.trim()||'admin123';const res=await window.SonicAPI.login(targetUsername,targetPwd);localStorage.setItem('sonic_token',res.access_token);localStorage.setItem('sonic_user',JSON.stringify(res.user));if(res.user&&res.user.must_change_password){setOldPassword(targetPwd);}onSuccess(res.user,res.access_token);}else if(activeTab==='register'){const res=await window.SonicAPI.register(username.trim(),email.trim(),password.trim());localStorage.setItem('sonic_token',res.access_token);localStorage.setItem('sonic_user',JSON.stringify(res.user));onSuccess(res.user,res.access_token);}else if(activeTab==='force_change'){const res=await window.SonicAPI.changePassword(oldPassword.trim(),newPassword.trim());localStorage.setItem('sonic_token',res.access_token);const user=JSON.parse(localStorage.getItem('sonic_user')||'{}');user.must_change_password=false;localStorage.setItem('sonic_user',JSON.stringify(user));onSuccess(user,res.access_token);}}catch(err){setError(err.message||(activeTab==='login'?'Tài khoản hoặc mật khẩu không chính xác. (Nếu bạn đã đổi mật khẩu trước đó, vui lòng nhập mật khẩu mới mà bạn đã tạo)':'Thao tác không thành công'));}finally{setLoading(false);}};const isForceMode=activeTab==='force_change';const canClose=!forceMandatory&&!isForceMode;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md"},/*#__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("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-teal-400"},isForceMode?'⚠️ Bắt Buộc Đổi Mật Khẩu Khởi Tạo':activeTab==='login'?'🔐 Đăng Nhập Hệ Thống':'📝 Đăng Ký Tài Khoản'),canClose&&/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),error&&/*#__PURE__*/React.createElement("div",{className:"mt-4 p-3 bg-red-900/40 border border-red-700 rounded-lg text-red-200 text-sm"},error),/*#__PURE__*/React.createElement("form",{onSubmit:handleSubmit,className:"mt-4 space-y-4"},isForceMode?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("p",{className:"text-xs text-amber-400 bg-amber-950/60 p-2.5 border border-amber-800/80 rounded leading-relaxed"},"🔒 Tài khoản của bạn đang dùng mật khẩu khởi tạo mặc định. Để bảo mật hệ thống, bạn phải đổi mật khẩu mới trước khi tiếp tục."),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Mật khẩu hiện tại (Mặc định: admin123)"),/*#__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-2 text-sm focus:outline-none focus:border-teal-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"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-2 text-sm focus:outline-none focus:border-teal-500"}))):/*#__PURE__*/React.createElement(React.Fragment,null,activeTab==='login'?/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Tên đăng nhập ",firstTime&&/*#__PURE__*/React.createElement("span",{className:"text-teal-400 font-normal"},"(Tùy chọn - Admin có thể bỏ trống)")),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Mặc định: admin",value:username,onChange:e=>setUsername(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})):/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Tên đăng nhập"),/*#__PURE__*/React.createElement("input",{type:"text",required:true,value:username,onChange:e=>setUsername(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})),activeTab==='register'&&/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Email"),/*#__PURE__*/React.createElement("input",{type:"email",required:true,value:email,onChange:e=>setEmail(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Mật khẩu ",activeTab==='login'&&firstTime&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-normal"},"(Lần đầu: admin123)")),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:activeTab==='login'?'current-password':'new-password',required:true,value:password,onChange:e=>setPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"}))),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150"},loading?'Đang xác thực...':isForceMode?'Đổi Mật Khẩu Ngay':activeTab==='login'?'Đăng Nhập System':'Tạo Tài Khoản Mới'),activeTab==='login'&&!isForceMode&&firstTime&&/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>{setUsername('admin');setPassword('admin123');setError('');},className:"w-full mt-2 py-1.5 bg-amber-950/60 hover:bg-amber-900/80 text-amber-300 border border-amber-800/60 text-xs font-semibold rounded-lg transition flex items-center justify-center gap-1.5"},"🔑 Điền Nhanh Tài Khoản Admin (admin / admin123)")),!isForceMode&&/*#__PURE__*/React.createElement("div",{className:"mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400"},activeTab==='login'?/*#__PURE__*/React.createElement("span",null,"Chưa có tài khoản? ",/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('register'),className:"text-teal-400 hover:underline"},"Đăng ký ngay")):/*#__PURE__*/React.createElement("span",null,"Đã có tài khoản? ",/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('login'),className:"text-teal-400 hover:underline"},"Đăng nhập")))));};const AIConfigModal=({isOpen,onClose,onConfigSaved})=>{if(!isOpen)return null;const defaultProvidersList=[{id:'openai_default',name:'OpenAI Official',provider_type:'openai',api_base_url:'https://api.openai.com/v1',api_key:'',model_name:'gpt-4o',temperature:0.7,is_active:true},{id:'openai_compat_default',name:'OpenAI Compatible (Ollama/LocalAI/DeepSeek)',provider_type:'openai_compatible',api_base_url:'http://localhost:11434/v1',api_key:'ollama',model_name:'deepseek-r1',temperature:0.7,is_active:false},{id:'anthropic_default',name:'Anthropic Claude',provider_type:'anthropic',api_base_url:'https://api.anthropic.com/v1',api_key:'',model_name:'claude-3-5-sonnet',temperature:0.7,is_active:false},{id:'gemini_default',name:'Google Gemini',provider_type:'gemini',api_base_url:'https://generativelanguage.googleapis.com',api_key:'',model_name:'gemini-1.5-pro',temperature:0.7,is_active:false}];const[providers,setProviders]=useState(defaultProvidersList);const[selectedId,setSelectedId]=useState('openai_default');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);useEffect(()=>{if(isOpen)loadConfigs();},[isOpen]);const loadConfigs=async()=>{setLoading(true);setError('');try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setProviders(data.providers);if(data.providers.length>0)setSelectedId(data.providers[0].id);}}catch(err){setError(err.message||'Lỗi nạp cấu hình AI');}finally{setLoading(false);}};const handleSave=async e=>{e.preventDefault();setMsg('');setError('');setLoading(true);try{const res=await window.SonicAPI.saveAIConfigs(providers);setMsg(res.message||'Đã lưu cấu hình AI Providers thành công!');if(onConfigSaved)onConfigSaved(providers);}catch(err){setError(err.message||'Lỗi khi lưu cấu hình AI');}finally{setLoading(false);}};const updateProviderField=(id,field,value)=>{setProviders(prev=>prev.map(p=>p.id===id?{...p,[field]:value}:p));};const activeProvider=providers.find(p=>p.id===selectedId)||providers[0];return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm select-none"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl 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-cyan-400 flex items-center gap-2"},"🤖 Quản Lý & Cấu Hình AI Providers"),/*#__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 grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"space-y-1.5 border-r border-[#383838] pr-3"},/*#__PURE__*/React.createElement("span",{className:"text-xs uppercase font-bold text-slate-400 block mb-2"},"Providers"),providers.map(p=>/*#__PURE__*/React.createElement("button",{key:p.id,tabIndex:0,onClick:()=>setSelectedId(p.id),onKeyDown:e=>{if(e.key===' '){e.preventDefault();updateProviderField(p.id,'is_active',!p.is_active);}},className:`w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId===p.id?'bg-cyan-600 text-white shadow':'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}`},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name),p.is_active&&/*#__PURE__*/React.createElement("span",{className:"w-2 h-2 rounded-full bg-emerald-400"}))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 mt-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();const rearrangeNewId='provider_'+Date.now();setProviders(prev=>[...prev,{id:rearrangeNewId,name:'New Provider',provider_type:'openai_compatible',api_base_url:'https://api.openai.com/v1',api_key:'',model_name:'gpt-4o-mini',temperature:0.7,is_active:false}]);setSelectedId(rearrangeNewId);},className:"flex-1 px-2 py-1 bg-emerald-700 hover:bg-emerald-600 text-white text-xs font-bold rounded"},"+ Thêm"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();if(confirm(`Xóa provider "${providers.find(p=>p.id===selectedId)?.name}"?`)){setProviders(prev=>{const filtered=prev.filter(p=>p.id!==selectedId);if(filtered.length>0)setSelectedId(filtered[0].id);return filtered;});}},className:"px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"},"Xóa"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:function(e){e.stopPropagation();var provs=providers.slice();var pos=-1;for(var pi=0;pi0){var tmp=provs[pos];provs[pos]=provs[pos-1];provs[pos-1]=tmp;setProviders(provs);}},className:"px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",title:"Di chuyển lên"},"▲"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:function(e){e.stopPropagation();var provs=providers.slice();var pos=-1;for(var pi=0;piupdateProviderField(activeProvider.id,'name',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-cyan-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"API Base URL (Endpoint)"),/*#__PURE__*/React.createElement("input",{type:"text",value:activeProvider.api_base_url||'',onChange:e=>updateProviderField(activeProvider.id,'api_base_url',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-cyan-300 focus:outline-none focus:border-cyan-500",placeholder:"https://api.openai.com/v1"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"API Key Cá Nhân"),/*#__PURE__*/React.createElement("input",{type:"password",value:activeProvider.api_key||'',onChange:e=>updateProviderField(activeProvider.id,'api_key',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500",placeholder:"sk-..."})),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"Model Name"),/*#__PURE__*/React.createElement("input",{type:"text",value:activeProvider.model_name||'',onChange:e=>updateProviderField(activeProvider.id,'model_name',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-amber-300 focus:outline-none focus:border-cyan-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"Temperature"),/*#__PURE__*/React.createElement("input",{type:"number",step:"0.1",min:"0",max:"2",value:activeProvider.temperature??0.7,onChange:e=>updateProviderField(activeProvider.id,'temperature',parseFloat(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-cyan-500"}))),/*#__PURE__*/React.createElement("div",{className:"pt-2 flex items-center justify-between"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-2 cursor-pointer text-xs text-slate-300"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:activeProvider.is_active,onChange:e=>updateProviderField(activeProvider.id,'is_active',e.target.checked),className:"rounded accent-cyan-500"}),"Kích hoạt Provider này"),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold text-xs rounded-lg shadow transition"},loading?'Đang lưu...':'Lưu Cấu Hình AI'))))));};const PluginManagerModal=({isOpen,onClose,pluginsData,onInsertInstrument})=>{if(!isOpen)return null;const[localData,setLocalData]=React.useState(pluginsData);const[sfUploadStatus,setSfUploadStatus]=React.useState('');const[sfToDelete,setSfToDelete]=React.useState(null);const[pmDirs,setPmDirs]=React.useState([]);const[pmScanning,setPmScanning]=React.useState(false);const[pmScanResult,setPmScanResult]=React.useState('');const[pmScanData,setPmScanData]=React.useState(null);// { vst_found, soundfonts } // Instrument bên trong mỗi soundfont (expand) — "Chèn vào Synth" qua onInsertInstrument const[pmSfExpanded,setPmSfExpanded]=React.useState({});const[pmSfInstruments,setPmSfInstruments]=React.useState({});const[pmSfLoading,setPmSfLoading]=React.useState({});// Force re-render sau khi định vị Carla (capabilities đổi) -const[pmCarlaVersion,setPmCarlaVersion]=React.useState(0);React.useEffect(()=>{if(isOpen){window.SonicAPI.listPlugins().then(data=>setLocalData(data)).catch(()=>setLocalData({vst_instruments:[],soundfonts:[]}));window.SonicAPI.getPluginDirs().then(d=>setPmDirs(d.plugin_dirs||[])).catch(()=>{});setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},50);}},[isOpen]);// Folder picker: +const[pmCarlaVersion,setPmCarlaVersion]=React.useState(0);// Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker) +const[pmCarlaPathInput,setPmCarlaPathInput]=React.useState('');React.useEffect(()=>{if(isOpen){window.SonicAPI.listPlugins().then(data=>setLocalData(data)).catch(()=>setLocalData({vst_instruments:[],soundfonts:[]}));window.SonicAPI.getPluginDirs().then(d=>setPmDirs(d.plugin_dirs||[])).catch(()=>{});setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},50);}},[isOpen]);// Folder picker: // 1) Neu page duoc Tauri serve (__TAURI__ co) -> dialog plugin invoke. // 2) Binh thuong UI chay tren http://127.0.0.1:8000 (engine) -> __TAURI__ // KHONG co (Tauri chi inject vao trang no serve; window.prompt cung @@ -384,7 +385,9 @@ try{const manual=window.prompt('Nhập đường dẫn thư mục plugin (VST / const toggleSfInstruments=async sf=>{const baseId=String(sf.id||'').replace('sf_','');setPmSfExpanded(prev=>({...prev,[baseId]:!prev[baseId]}));if(!pmSfInstruments[baseId]&&!pmSfLoading[baseId]){setPmSfLoading(prev=>({...prev,[baseId]:true}));try{const r=await window.SonicAPI.listSoundfontInstruments(baseId);setPmSfInstruments(prev=>({...prev,[baseId]:r&&r.presets||[]}));}catch(e){setPmSfInstruments(prev=>({...prev,[baseId]:[]}));}finally{setPmSfLoading(prev=>({...prev,[baseId]:false}));}}};// Định vị Carla.exe — bản Windows là zip portable: KHÔNG cài đặt, KHÔNG dùng // biến môi trường PATH nên heuristic không tìm thấy → user tự chọn thư mục // chứa carla.exe (folder picker native) → lưu config phía server. -const locateCarla=async()=>{try{let picked=null;try{const d=await window.SonicAPI.pickPluginDir();if(d&&typeof d.path==='string'&&d.path)picked=d.path;}catch(e){/* fallthrough */}if(!picked&&window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke){try{const sel=await window.__TAURI__.core.invoke('plugin:dialog|open',{options:{directory:true,multiple:false}});if(typeof sel==='string'&&sel)picked=sel;}catch(e){/* fallthrough */}}if(!picked){showToast('Không mở được hộp thoại chọn thư mục','error');return;}const r=await window.SonicAPI.setCarlaPath(picked);if(r&&r.success&&r.carla_path){window.SonicRuntime.capabilities=r;document.documentElement.dataset.carla=r.features&&r.features.carla_local?'1':'0';setPmCarlaVersion(v=>v+1);showToast('Đã định vị Carla: '+r.carla_path,'success');}else{showToast('Không tìm thấy carla.exe trong thư mục đã chọn','error');}}catch(err){showToast('Lỗi định vị Carla: '+(err.message||err),'error');}};// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. +const locateCarla=async()=>{try{let picked=null;try{const d=await window.SonicAPI.pickPluginDir();if(d&&typeof d.path==='string'&&d.path)picked=d.path;}catch(e){/* fallthrough */}if(!picked&&window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke){try{const sel=await window.__TAURI__.core.invoke('plugin:dialog|open',{options:{directory:true,multiple:false}});if(typeof sel==='string'&&sel)picked=sel;}catch(e){/* fallthrough */}}if(!picked){showToast('Không mở được hộp thoại chọn thư mục','error');return;}const r=await window.SonicAPI.setCarlaPath(picked);if(r&&r.success&&r.carla_path){window.SonicRuntime.capabilities=r;document.documentElement.dataset.carla=r.features&&r.features.carla_local?'1':'0';setPmCarlaVersion(v=>v+1);showToast('Đã định vị Carla: '+r.carla_path,'success');}else{showToast('Không tìm thấy carla.exe trong thư mục đã chọn','error');}}catch(err){showToast('Lỗi định vị Carla: '+(err.message||err),'error');}};// Khai báo thư mục Carla trực tiếp (nhập tay) — tương tự Định vị nhưng +// không cần hộp thoại chọn thư mục. +const saveCarlaInput=async()=>{const p=(pmCarlaPathInput||'').trim();if(!p){showToast('Nhập đường dẫn thư mục chứa carla.exe','warning');return;}try{const r=await window.SonicAPI.setCarlaPath(p);if(r&&r.success&&r.carla_path){window.SonicRuntime.capabilities=r;document.documentElement.dataset.carla=r.features&&r.features.carla_local?'1':'0';setPmCarlaVersion(v=>v+1);setPmCarlaPathInput('');showToast('Đã lưu Carla: '+r.carla_path,'success');}else{showToast('Không tìm thấy carla.exe trong đường dẫn đã nhập','error');}}catch(err){showToast('Lỗi: '+(err.message||err),'error');}};// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. const saveAndScanDirs=async()=>{setPmScanning(true);setPmScanResult('');setPmScanData(null);try{await window.SonicAPI.savePluginDirs({plugin_dirs:pmDirs});const scan=await window.SonicAPI.scanPluginDirs();setPmScanData({vst_found:scan.vst_found||[],soundfonts:scan.soundfonts||[]});const data=await window.SonicAPI.listPlugins();setLocalData(data);try{const cat=await window.SonicAPI.getSoundfontCatalog();window.__soundfontCatalog=cat;}catch(_){}setPmScanResult(`VST: ${scan.vst_count||0} | SoundFonts: ${scan.soundfont_count||0}`);showToast(`Scan xong: ${scan.vst_count||0} VST, ${scan.soundfont_count||0} SoundFonts.`,'success');}catch(err){setPmScanResult('Scan failed: '+(err.message||err));showToast('Scan failed: '+(err.message||err),'error');}finally{setPmScanning(false);}};const handleUploadSF=async e=>{const file=e.target.files?.[0];if(!file)return;setSfUploadStatus('Uploading...');try{const result=await window.SonicAPI.uploadSoundFont(file);setSfUploadStatus('Uploaded: '+result.name);// Refresh plugin list and catalog const data=await window.SonicAPI.listPlugins();setLocalData(data);try{const cat=await window.SonicAPI.getSoundfontCatalog();window.__soundfontCatalog=cat;}catch(_){}}catch(err){setSfUploadStatus('Error: '+err.message);}};const[pmTab,setPmTab]=React.useState('soundfont');return React.createElement('div',{className:'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm',onClick:onClose},React.createElement('div',{className:'bg-[#1e1e1e] border border-[#383838] rounded-xl shadow-2xl w-full max-w-3xl p-0 text-slate-200 overflow-hidden flex flex-col relative',style:{maxHeight:'80vh'},onClick:e=>e.stopPropagation()},// ── In-app folder picker (Plugin Directories) ───────────────────── pmPicker&&React.createElement('div',{className:'absolute inset-0 z-10 bg-[#171717]/97 flex flex-col',style:{padding:16}},React.createElement('div',{className:'flex items-center justify-between mb-2'},React.createElement('div',{className:'text-xs font-bold text-violet-300 uppercase'},'Chọn thư mục plugin'),React.createElement('button',{onClick:()=>setPmPicker(null),className:'text-zinc-500 hover:text-zinc-200 transition'},React.createElement('i',{'data-lucide':'x',className:'w-4 h-4'}))),React.createElement('div',{className:'flex items-center gap-2 mb-2'},React.createElement('button',{onClick:()=>{if(pmPicker.parent)browsePluginDir(pmPicker.parent);},disabled:!pmPicker.parent,className:'px-2 py-1 bg-zinc-800 hover:bg-zinc-700 rounded text-[11px] text-zinc-300 disabled:opacity-30 shrink-0'},'Lên'),React.createElement('div',{className:'flex-1 text-[11px] text-zinc-400 font-mono truncate',title:pmPicker.path||''},pmPicker.path||(pmPicker.loading?'Đang tải...':'My Computer'))),pmPicker.loading?React.createElement('div',{className:'flex-1 flex items-center justify-center text-zinc-500 text-xs'},'Đang tải...'):pmPicker.dirs?pmPicker.dirs.length===0?React.createElement('div',{className:'flex-1 flex items-center justify-center text-zinc-600 text-xs italic'},'Thư mục trống'):React.createElement('div',{className:'flex-1 overflow-y-auto space-y-1'},pmPicker.dirs.map((d,i)=>React.createElement('div',{key:'pd_'+i,className:'flex items-center gap-2 px-2 py-1.5 rounded hover:bg-violet-900/30 cursor-pointer group',onClick:()=>browsePluginDir(d.path)},React.createElement('i',{'data-lucide':'folder',className:'w-3.5 h-3.5 text-amber-500/80 shrink-0'}),React.createElement('span',{className:'flex-1 text-[11px] text-zinc-300 truncate'},d.name),React.createElement('i',{'data-lucide':'chevron-right',className:'w-3 h-3 text-zinc-600 group-hover:text-violet-400 shrink-0'})))):pmPicker.roots?pmPicker.roots.length===0?React.createElement('div',{className:'flex-1 flex items-center justify-center text-zinc-600 text-xs'},'Không tìm thấy ổ đĩa'):React.createElement('div',{className:'flex-1 overflow-y-auto space-y-1'},pmPicker.roots.map((r,i)=>React.createElement('div',{key:'pr_'+i,className:'flex items-center gap-2 px-2 py-1.5 rounded hover:bg-violet-900/30 cursor-pointer',onClick:()=>browsePluginDir(r.path)},React.createElement('i',{'data-lucide':'hard-drive',className:'w-3.5 h-3.5 text-cyan-500/80 shrink-0'}),React.createElement('span',{className:'flex-1 text-[11px] text-zinc-300'},r.name||r.path)))):null,React.createElement('div',{className:'flex items-center justify-end gap-2 mt-2 pt-2 border-t border-[#383838]'},React.createElement('button',{onClick:()=>setPmPicker(null),className:'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white text-xs rounded'},'Hủy'),React.createElement('button',{onClick:confirmPluginDir,disabled:!pmPicker.path,className:'px-3 py-1.5 bg-violet-700 hover:bg-violet-600 text-white text-xs font-semibold rounded disabled:opacity-30'},'Chọn thư mục này'))),// Header @@ -392,7 +395,7 @@ React.createElement('div',{className:'flex items-center justify-between px-5 py- React.createElement('div',{className:'flex flex-1 overflow-hidden',style:{minHeight:'300px'}},// Left sidebar React.createElement('div',{className:'w-40 shrink-0 border-r border-[#383838] bg-[#1a1a1a] p-3 flex flex-col gap-2'},['vst','soundfont'].map(tab=>React.createElement('button',{key:tab,onClick:()=>setPmTab(tab),className:`w-full py-2 text-xs font-bold rounded transition border ${pmTab===tab?tab==='vst'?'bg-violet-900 border-violet-700 text-violet-200':'bg-amber-900 border-amber-700 text-amber-200':'bg-zinc-800 border-transparent text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700'} flex items-center gap-2 px-3`},React.createElement('i',{'data-lucide':tab==='vst'?'cpu':'music',className:'w-3.5 h-3.5'}),tab==='vst'?'VST Instruments':'SoundFonts'))),// Right content React.createElement('div',{className:'flex-1 overflow-y-auto p-4 bg-[#1e1e1e]'},!localData?React.createElement('div',{className:'flex items-center justify-center h-full text-zinc-500 text-xs'},'Loading...'):React.createElement('div',{className:'space-y-2'},pmTab==='vst'?localData.vst_instruments?.length===0?React.createElement('div',{className:'flex items-center justify-center h-32 text-zinc-500 text-xs'},'No VST instruments found on server.'):localData.vst_instruments.map((v,i)=>React.createElement('div',{key:i,className:'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-violet-800/50 transition'},React.createElement('div',{className:'flex items-center gap-3'},React.createElement('div',{className:'w-8 h-8 rounded bg-violet-900/30 flex items-center justify-center'},React.createElement('i',{'data-lucide':'cpu',className:'w-4 h-4 text-violet-400'})),React.createElement('div',null,React.createElement('div',{className:'text-xs font-semibold text-slate-200'},v.name||v.id),React.createElement('div',{className:'text-[10px] text-zinc-500'},v.type||'VST3'))),React.createElement('span',{className:'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30'},v.type||'VST3'))):localData.soundfonts?.length===0?React.createElement('div',{className:'flex items-center justify-center h-32 text-zinc-500 text-xs'},'No SoundFonts found. Upload one below.'):localData.soundfonts.map((sf,i)=>{const baseId=String(sf.id||'').replace('sf_','');const expanded=!!pmSfExpanded[baseId];const insts=pmSfInstruments[baseId]||[];const loading=!!pmSfLoading[baseId];return React.createElement('div',{key:i,className:'bg-[#252525] rounded-lg border border-[#333] hover:border-amber-800/50 transition group'},React.createElement('div',{className:'flex items-center justify-between px-4 py-3 cursor-pointer',onClick:()=>toggleSfInstruments(sf)},React.createElement('div',{className:'flex items-center gap-3'},React.createElement('div',{className:'w-8 h-8 rounded bg-amber-900/30 flex items-center justify-center'},React.createElement('i',{'data-lucide':'music',className:'w-4 h-4 text-amber-400'})),React.createElement('div',null,React.createElement('div',{className:'text-xs font-semibold text-slate-200'},sf.display||sf.name||sf.id),React.createElement('div',{className:'text-[10px] text-zinc-500'},(sf.file||sf.name)+(insts.length?' — '+insts.length+' instruments':'')))),React.createElement('div',{className:'flex items-center gap-2'},React.createElement('span',{className:'text-[10px] text-zinc-500'},expanded?'▾':'▸'),React.createElement('button',{onClick:e=>{e.stopPropagation();setSfToDelete(sf);},className:'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'},'Delete'))),expanded&&React.createElement('div',{className:'border-t border-[#333] px-3 py-1 max-h-40 overflow-y-auto'},loading?React.createElement('p',{className:'text-[10px] text-zinc-500 italic py-1'},'Đang đọc instruments...'):insts.length===0?React.createElement('p',{className:'text-[10px] text-zinc-500 italic py-1'},'Không có instrument (SF3 cần chuyển đổi trước)'):insts.map((p,pi)=>React.createElement('div',{key:'si_'+pi,className:'flex items-center gap-2 py-1 text-[11px]'},React.createElement('span',{className:'text-zinc-500 font-mono w-24 shrink-0 text-[9px]'},'B'+(p.bank||0)+' P'+(p.program||0)),React.createElement('span',{className:'flex-1 truncate text-zinc-300'},p.name||'Program '+p.program),React.createElement('button',{onClick:()=>onInsertInstrument&&onInsertInstrument({instrumentId:'sf_'+baseId,bank:p.bank||0,program:p.program||0,name:p.name||'Program '+p.program,displayName:(sf.display||sf.name||sf.id)+' — '+(p.name||'Program '+p.program)}),className:'text-[10px] bg-amber-800 hover:bg-amber-700 text-white px-2 py-0.5 rounded transition shrink-0'},'Chèn vào Synth')))));})),// ── Carla Bridge section (desktop) — định vị carla.exe portable ── -window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.runtime==='desktop'&&React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('h4',{className:'text-xs font-bold text-teal-400 uppercase mb-2'},'Carla Bridge (VSTi native GUI)'),React.createElement('p',{className:'text-[10px] text-zinc-500 mb-2 break-all'},window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_path?'Đã định vị: '+window.SonicRuntime.capabilities.features.carla_path:'Chưa tìm thấy Carla. Bản Windows là zip portable (không cài đặt, không dùng PATH) — nhấn "Định vị Carla..." và chọn thư mục chứa carla.exe.'),React.createElement('div',{className:'flex gap-2'},React.createElement('button',{onClick:locateCarla,className:'px-3 py-1.5 bg-teal-800 hover:bg-teal-700 text-white text-xs font-semibold rounded transition flex items-center gap-1'},React.createElement('i',{'data-lucide':'folder-search',className:'w-3 h-3'}),'Định vị Carla...'),window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local&&React.createElement('button',{onClick:()=>{window.SonicAPI.openInCarla().then(function(r){if(r&&r.success)showToast('Đã mở Carla','success');}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});},className:'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1'},React.createElement('i',{'data-lucide':'play',className:'w-3 h-3'}),'Mở Carla'))),// Plugin directories section (folder picker + save + scan) +window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.runtime==='desktop'&&React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('h4',{className:'text-xs font-bold text-teal-400 uppercase mb-2'},'Carla Bridge (VSTi native GUI)'),React.createElement('p',{className:'text-[10px] text-zinc-500 mb-2 break-all'},window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_path?'Đã định vị: '+window.SonicRuntime.capabilities.features.carla_path:'Chưa tìm thấy Carla. Bản Windows là zip portable (không cài đặt, không dùng PATH) — nhấn "Định vị Carla..." và chọn thư mục chứa carla.exe.'),React.createElement('div',{className:'flex gap-2'},React.createElement('button',{onClick:locateCarla,className:'px-3 py-1.5 bg-teal-800 hover:bg-teal-700 text-white text-xs font-semibold rounded transition flex items-center gap-1'},React.createElement('i',{'data-lucide':'folder-search',className:'w-3 h-3'}),'Định vị Carla...'),window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local&&React.createElement('button',{onClick:()=>{window.SonicAPI.openInCarla().then(function(r){if(r&&r.success)showToast('Đã mở Carla','success');}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});},className:'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1'},React.createElement('i',{'data-lucide':'play',className:'w-3 h-3'}),'Mở Carla')),React.createElement('div',{className:'flex gap-2 mt-2'},React.createElement('input',{type:'text',placeholder:'Hoặc nhập thư mục chứa carla.exe (VD: D:/Tools/Carla)',value:pmCarlaPathInput,onChange:e=>setPmCarlaPathInput(e.target.value),onKeyDown:e=>{if(e.key==='Enter')saveCarlaInput();},className:'flex-1 bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-teal-600'}),React.createElement('button',{onClick:saveCarlaInput,className:'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition shrink-0'},'Lưu'))),// Plugin directories section (folder picker + save + scan) React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('div',{className:'flex items-center justify-between mb-2'},React.createElement('h4',{className:'text-xs font-bold text-zinc-400 uppercase'},'Plugin Directories'),React.createElement('button',{className:'px-3 py-1.5 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0',title:'Add plugin directory (VST / SoundFont)',onClick:pickPluginFolder},React.createElement('i',{'data-lucide':'plus',className:'w-3 h-3'}),'Add Directory')),pmDirs.length===0&&React.createElement('p',{className:'text-[10px] text-zinc-600 mb-2 italic'},'Chưa có thư mục nào. Nhấn Add Directory để chọn thư mục chứa VST / SoundFont.'),pmDirs.map((dir,idx)=>React.createElement('div',{key:'pdir_'+idx,className:'flex items-center gap-2 mb-1.5 bg-zinc-800/70 border border-zinc-700 rounded px-2 py-1.5'},React.createElement('button',{className:'text-zinc-500 hover:text-red-400 transition shrink-0',title:'Remove directory',onClick:()=>removePluginDir(dir)},React.createElement('i',{'data-lucide':'x',className:'w-3.5 h-3.5'})),React.createElement('span',{className:'flex-1 text-[11px] text-zinc-300 font-mono truncate',title:dir},dir),React.createElement('i',{'data-lucide':'folder',className:'w-3 h-3 text-zinc-600 shrink-0'}))),React.createElement('div',{className:'flex gap-2 items-center mt-2'},React.createElement('button',{className:'px-4 py-2 bg-emerald-800 hover:bg-emerald-700 text-white text-xs font-semibold rounded transition flex items-center gap-1',onClick:saveAndScanDirs},React.createElement('i',{'data-lucide':'search',className:'w-3 h-3'}),'Scan'),pmScanning&&React.createElement('span',{className:'text-[10px] text-emerald-400'},'Scanning...'),pmScanResult&&React.createElement('span',{className:'text-[10px] text-zinc-400'},pmScanResult)),pmScanData&&React.createElement('div',{className:'mt-3 space-y-2 max-h-40 overflow-y-auto'},React.createElement('div',{className:'text-[10px] font-bold text-violet-300 uppercase flex items-center gap-1'},React.createElement('i',{'data-lucide':'cpu',className:'w-3 h-3'}),'VST Instruments ('+pmScanData.vst_found.length+')'),pmScanData.vst_found.length===0?React.createElement('p',{className:'text-[10px] text-zinc-600 italic'},'Không tìm thấy VST.'):pmScanData.vst_found.map((v,i)=>React.createElement('div',{key:'sv_'+i,className:'flex items-center gap-2 text-[11px] text-zinc-300'},React.createElement('span',{className:'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate'},v.type||'VST'),React.createElement('span',{className:'truncate'},v.name),React.createElement('span',{className:'text-[9px] text-zinc-600 font-mono truncate ml-auto'},v.dir))),React.createElement('div',{className:'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2'},React.createElement('i',{'data-lucide':'music',className:'w-3 h-3'}),'SoundFonts ('+pmScanData.soundfonts.length+')'),pmScanData.soundfonts.length===0?React.createElement('p',{className:'text-[10px] text-zinc-600 italic'},'Không tìm thấy SoundFont.'):pmScanData.soundfonts.map((s,i)=>React.createElement('div',{key:'ss_'+i,className:'flex items-center gap-2 text-[11px] text-zinc-300'},React.createElement('span',{className:'truncate'},s.name),React.createElement('span',{className:'text-[9px] text-zinc-600 font-mono truncate ml-auto'},s.dir))))),// Upload section (bottom of right panel) React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('h4',{className:'text-xs font-bold text-zinc-400 mb-3 uppercase'},pmTab==='vst'?'Add VST Directory':'Upload SoundFont'),pmTab==='vst'?React.createElement('div',{className:'flex gap-2'},React.createElement('input',{type:'text',placeholder:'/opt/daw_engine/vst3',className:'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-violet-600'}),React.createElement('button',{className:'px-4 py-2 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition',onClick:async()=>{try{const data=await window.SonicAPI.listPlugins();setLocalData(data);showToast('Scanned VST directory.','info');}catch(err){showToast('Scan failed: '+err.message,'error');}}},'Scan')):React.createElement('div',{className:'space-y-2'},React.createElement('label',{className:'flex items-center gap-3 px-4 py-3 border-2 border-dashed border-zinc-700 rounded-lg cursor-pointer hover:border-amber-600/50 bg-zinc-800/40 transition'},React.createElement('i',{'data-lucide':'upload',className:'w-5 h-5 text-zinc-400'}),React.createElement('span',{className:'text-xs text-zinc-400'},'Click to upload .sf2 / .sf3 file'),React.createElement('input',{type:'file',accept:'.sf2,.sf3',onChange:async e=>{const file=e.target.files?.[0];if(!file)return;setSfUploadStatus('Uploading...');try{await window.SonicAPI.uploadSoundFont(file);setSfUploadStatus('Uploaded: '+file.name);const data=await window.SonicAPI.listPlugins();setLocalData(data);}catch(err){setSfUploadStatus('Error: '+err.message);}},className:'hidden'})),sfUploadStatus&&React.createElement('p',{className:'text-[10px] text-zinc-500'},sfUploadStatus)))))),// Status bar at bottom React.createElement('div',{className:'px-5 py-2 bg-[#1a1a1a] border-t border-[#383838] flex items-center justify-between text-[10px] text-zinc-500'},React.createElement('span',null,'VST: ',localData?.vst_instruments?.length||0,' | SoundFonts: ',localData?.soundfonts?.length||0),React.createElement('span',null,'Last scanned: ',new Date().toLocaleTimeString())),// Delete confirmation modal @@ -1624,4 +1627,6 @@ const ctx=getAudioContext();const elapsed=ctx.currentTime-startAudioTimeRef.curr // (quét 8 bars hiển thị "0 đến 8"): nhập 8 → selection tới vạch 8. setSelectionEnd(t);setNumberBar(Math.max(0,b-beginBar));},className:"w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"#"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",value:numberBar,readOnly:true,className:"w-[60px] bg-black text-zinc-400 text-[18px] px-1 py-0 rounded border border-zinc-800 text-center font-mono"}),/*#__PURE__*/React.createElement("button",{onClick:()=>setSelectionFollowsTempo(prev=>!prev),className:`px-1.5 py-0 text-[18px] rounded border font-bold ${selectionFollowsTempo?'bg-amber-700 text-white border-amber-500':'bg-zinc-800 text-zinc-400 border-zinc-700'}`,title:selectionFollowsTempo?"Selection theo tempo (đổi BPM → selection thay đổi)":"Selection theo thời gian (cố định)"},selectionFollowsTempo?"♪T":"⏱T"),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-6 bg-zinc-800 mx-1"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"Start:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selLeft!==null?formatTime(selLeft):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));clearLocalSelection();setSelectionMode('global');setSelectionStart(secs);}},className:"w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"End:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selRight!==null?formatTime(selRight):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));setSelectionEnd(secs);}},className:"w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"Len:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selLeft!==null&&selRight!==null?formatTime(Math.abs(selRight-selLeft)):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3&&selLeft!==null){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));setSelectionEnd(selLeft+secs);}},className:"w-24 bg-black text-amber-300 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>setShowExportPanel(p=>!p),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showExportPanel?'bg-cyan-900 text-cyan-300':'text-zinc-500 hover:text-zinc-300'}`,title:`Export (floating modal)`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"text-[7px] opacity-60"},showExportPanel?panelPositions.export[0].toUpperCase():'')),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowAIPanel(p=>!p),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showAIPanel?'bg-purple-900 text-purple-300':'text-zinc-500 hover:text-zinc-300'}`,title:`AI Panel (${panelPositions.ai})`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"text-[7px] opacity-60"},showAIPanel?panelPositions.ai[0].toUpperCase():'')),/*#__PURE__*/React.createElement("button",{onClick:()=>window.__toggleMixerRef(),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMixer?'bg-indigo-900 text-indigo-300':'text-zinc-500 hover:text-zinc-300'}`,title:"Mixer Panel (F7)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders-horizontal",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>window.__toggleMediaExplorerRef(),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMediaExplorer?'bg-emerald-900 text-emerald-300':'text-zinc-500 hover:text-zinc-300'}`,title:"Media Explorer Panel (F6)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"folder-open",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("span",{className:"w-[1px] h-3 bg-zinc-800 mx-1"}),/*#__PURE__*/React.createElement("span",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"info",className:"w-3 h-3 text-zinc-600"})),prHint?/*#__PURE__*/React.createElement("span",{className:"text-cyan-400"},prHint):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 italic"},"Adaptive tips: hover vào vùng làm việc để xem hướng dẫn")))),contextMenu&&(contextMenu.isSubTab?/*#__PURE__*/React.createElement("div",{className:"fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",style:{left:Math.min(contextMenu.x,window.innerWidth-260),top:contextMenu.y+200>window.innerHeight?undefined:contextMenu.y,bottom:contextMenu.y+200>window.innerHeight?window.innerHeight-contextMenu.y:undefined,maxHeight:'60vh'},onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"px-3 py-1 text-xs text-zinc-500 font-bold uppercase border-b border-zinc-800 pb-1 mb-1"},"Selection: ",formatTime(subTabs.find(s=>s.id===contextMenu.subTabId)?.selectionStart||0)," - ",formatTime(subTabs.find(s=>s.id===contextMenu.subTabId)?.selectionEnd||0)),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabCut(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-rose-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Cut"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+X")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabCopy(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5 text-zinc-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Copy"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+C")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabPaste(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Paste"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+V")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabDelete(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3.5 h-3.5 text-red-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Delete Selected Segment"),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 text-xs font-semibold font-mono ml-auto"},"Del")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabLoop(contextMenu.subTabId,4);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"repeat",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Loop Selection 4 times"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+L"))):/*#__PURE__*/React.createElement("div",{className:"fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",style:{left:Math.min(contextMenu.x,window.innerWidth-260),top:contextMenu.y+260>window.innerHeight?undefined:contextMenu.y,bottom:contextMenu.y+260>window.innerHeight?window.innerHeight-contextMenu.y:undefined,maxHeight:'60vh'},onClick:e=>e.stopPropagation()},contextMenu.sectionId?/*#__PURE__*/React.createElement("button",{onClick:contextMenuEditSection,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-edit",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Edit Section"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+E")):/*#__PURE__*/React.createElement("button",{onClick:contextMenuEdit,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-edit",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Edit"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+E")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuSplit,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Split"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"S")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuMerge,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"combine",className:"w-3.5 h-3.5 text-purple-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Merge"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+M")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("div",{className:"px-3 py-0.5 text-[10px] text-zinc-500 font-bold uppercase tracking-wider"},"Insert"),!sessionTabs.some(s=>s.id===activeTab)&&/*#__PURE__*/React.createElement("button",{onClick:()=>{insertSectionAtPlayhead();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"folder-plus",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Section")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertMidiItemAtPlayhead();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5 text-purple-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert MIDI Item")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertSoundClipAtCursor();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-audio",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Sound Clip")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertTrackBelow();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus-square",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Track")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:contextMenuCopy,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5 text-zinc-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Copy"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+C")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuCut,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-rose-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Cut"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+X")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuPaste,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Paste"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+V")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:contextMenuDelete,className:"w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3.5 h-3.5 text-red-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Delete"),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 text-xs font-semibold font-mono ml-auto pl-8"},"Del")))),toastMessage&&/*#__PURE__*/React.createElement("div",{className:"absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"bell",className:`w-4 h-4 ${toastMessage.type==='success'?'text-emerald-400':toastMessage.type==='error'?'text-rose-400':toastMessage.type==='warning'?'text-amber-400':'text-cyan-400'}`})),toastMessage.text,toastMessage.onActionClick&&/*#__PURE__*/React.createElement("button",{onClick:()=>{toastMessage.onActionClick();setToastMessage(null);},className:"ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"},toastMessage.actionText||'Tải về'))),appWarningModal&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#222222] border border-zinc-800 rounded-lg shadow-2xl p-6 max-w-md w-full text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 text-cyan-400 font-bold"},/*#__PURE__*/React.createElement("h3",{className:"text-base font-bold"},appWarningModal.title)),/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-300 mb-5 leading-relaxed whitespace-pre-line"},appWarningModal.message),/*#__PURE__*/React.createElement("div",{className:"flex justify-end gap-2"},appWarningModal.isAlert?/*#__PURE__*/React.createElement("button",{onClick:()=>setAppWarningModal(null),className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"},"Đóng"):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("button",{onClick:()=>setAppWarningModal(null),className:"px-4 py-2 bg-zinc-700 hover:bg-zinc-600 active:bg-zinc-800 text-slate-300 rounded text-xs font-semibold transition"},"Hủy"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const fn=appWarningModal.onConfirm;setAppWarningModal(null);if(fn)fn();},className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"},"Xác nhận"))))),tabContextMenu&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[190]",onClick:()=>setTabContextMenu(null),onContextMenu:e=>{e.preventDefault();setTabContextMenu(null);}}),/*#__PURE__*/React.createElement("div",{className:"fixed bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl p-2 z-[200] flex gap-1.5",style:{top:tabContextMenu.y,left:tabContextMenu.x},onClick:e=>e.stopPropagation()},['#f43f5e','#f59e0b','#10b981','#06b6d4','#8b5cf6','#64748b'].map(color=>/*#__PURE__*/React.createElement("button",{key:color,className:"w-5 h-5 rounded-full border border-zinc-700 hover:scale-110 active:scale-95 transition",style:{backgroundColor:color},onClick:()=>{handleSetTabColor(tabContextMenu.tabId,tabContextMenu.tabType,color);setTabContextMenu(null);}})))),/*#__PURE__*/React.createElement(AuthModal,{isOpen:authModalOpen,mode:authMode,forceMandatory:isMandatoryLogin,onClose:()=>setAuthModalOpen(false),onSuccess:handleAuthSuccess}),/*#__PURE__*/React.createElement(ProfileModal,{isOpen:profileModalOpen,onClose:()=>setProfileModalOpen(false),tracks:tracks,setTracks:setTracks,setSelectedTrackId:setSelectedTrackId,projectName:projectName,setProjectName:setProjectName,currentProjectId:currentProjectId,setCurrentProjectId:setCurrentProjectId,showToast:showToast,loadAudioBuffersForTracks:loadAudioBuffersForTracks,setAppWarningModal:setAppWarningModal,bpm:bpm,setBpm:setBpm,setMasteringSettings:setMasteringSettings,setSessionTabs:setSessionTabs,setSubTabs:setSubTabs,trackMidiChannelsRef:trackMidiChannelsRef}),/*#__PURE__*/React.createElement(SaveProjectModal,{isOpen:saveProjectModalOpen,onClose:()=>setSaveProjectModalOpen(false),projectName:projectName,onSaveCloud:(newName,existingId)=>{handleSaveCloudProject(newName,existingId);},onSaveLocal:newName=>{handleSaveLocalProject(newName);}}),/*#__PURE__*/React.createElement(SaveAsModal,{isOpen:saveAsModalOpen,onClose:()=>setSaveAsModalOpen(false),projectName:projectName,onSaveCloud:newName=>{handleSaveAsCloud(newName);},onSaveLocal:newName=>{handleExportSFS(newName);setProjectName(newName);localStorage.setItem('sonic_project_name',newName);}}),/*#__PURE__*/React.createElement(OpenProjectModal,{isOpen:openProjectModalOpen,onClose:()=>setOpenProjectModalOpen(false),onOpenCloud:(projId,projName)=>{setOpenProjectModalOpen(false);handleOpenProject(projId,projName);},onOpenLocal:()=>{setOpenProjectModalOpen(false);handleImportSFS();}}),/*#__PURE__*/React.createElement(AIConfigModal,{isOpen:aiConfigModalOpen,onClose:()=>setAiConfigModalOpen(false),onConfigSaved:providers=>{setAiProviders(providers);const active=providers.find(p=>p.is_active)||providers[0];if(active)setSelectedProviderId(active.id);}}),/*#__PURE__*/React.createElement(AboutModal,{isOpen:aboutModalOpen,onClose:()=>setAboutModalOpen(false)}),/*#__PURE__*/React.createElement(HelpModal,{isOpen:helpModalOpen,onClose:()=>setHelpModalOpen(false),lang:prefs.language}),/*#__PURE__*/React.createElement(PreferencesModal,{isOpen:preferencesModalOpen,onClose:()=>setPreferencesModalOpen(false),prefs:prefs,onPrefsChange:handlePrefsChange}),/*#__PURE__*/React.createElement(SystemManagerModal,{isOpen:systemManagerModalOpen,onClose:()=>setSystemManagerModalOpen(false)}),/*#__PURE__*/React.createElement(PluginManagerModal,{isOpen:pluginManagerModalOpen,onClose:()=>setPluginManagerModalOpen(false),pluginsData:pluginsData,onInsertInstrument:inst=>{// Chèn instrument (soundfont bank/program) vào track đang chọn — nút Synth const tid=selectedTrackId||activeTracks&&activeTracks[0]&&activeTracks[0].id;if(tid&&inst&&inst.instrumentId){setTrackInstrumentWithProgram(tid,inst.instrumentId,inst.program,inst.displayName||inst.name,inst.bank);showToast('Đã chèn nhạc cụ: '+(inst.displayName||inst.name),'success');}setPluginManagerModalOpen(false);}}),/*#__PURE__*/React.createElement(AIPresetModal,{isOpen:aiPresetModalOpen,onClose:()=>{setAiPresetModalOpen(false);setAiPresetVersion(v=>v+1);}}),/*#__PURE__*/React.createElement(MasteringModal,{isOpen:showMasteringModal,onClose:()=>setShowMasteringModal(false),masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings}),fxRackTarget&&/*#__PURE__*/React.createElement(FXRackModal,{track:tracks.find(t=>t.id===fxRackTarget.trackId)||null,onUpdateTrack:updateTrackProp,onClose:()=>setFxRackTarget(null)}),/*#__PURE__*/React.createElement(ExportModal,{open:showExportPanel,onClose:()=>setShowExportPanel(false),exportSettings:exportSettings,setExportSettings:setExportSettings,isExporting:isExporting,onExport:triggerWavExport,onBounce:triggerBounceExport}),instrumentSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:closeInstrumentSelector},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Select Instrument"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("select",{value:instrumentSelectorTrackId||'',onChange:e=>{var tid=e.target.value;if(tid){openInstrumentSelector(tid);}},className:"bg-[#1e1e1e] text-zinc-300 border border-zinc-700 rounded px-2 py-1 text-xs font-mono cursor-pointer"},activeTracks.map(function(at){return/*#__PURE__*/React.createElement("option",{key:at.id,value:at.id},at.name+' ('+(at.instrumentName||'Synth')+')');})),/*#__PURE__*/React.createElement("button",{onClick:closeInstrumentSelector,className:"text-slate-400 hover:text-slate-200"},"\u2715"))),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:sfPresetSearchQuery,onChange:e=>setSfPresetSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border border-zinc-700 rounded px-2 py-1.5 outline-none my-3 text-sm"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-4",style:{height:"420px"}},/*#__PURE__*/React.createElement("div",{className:"w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setSelectedSoundFontId(null);setSynthCategory(null);},className:"w-full text-left px-3 py-2 text-sm rounded "+(!selectedSoundFontId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},"All Instruments"),(instrumentSelectorData?.soundfonts||[]).map(sf=>{const sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;const sfName=sf.display||sf.name||sf.id;return/*#__PURE__*/React.createElement("button",{key:sfId,onClick:()=>{setSelectedSoundFontId(sfId);setSfPresets(null);const baseId=sfId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(baseId).then(data=>{if(data&&data.presets){const mapping=data.presets.map(p=>({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)}));setSfPresets(mapping);setInstrumentSelectorData(prev=>prev?{...prev,soundfonts:(prev.soundfonts||[]).map(s=>s.id===sf.id?{...s,presets:data.presets}:s)}:prev);}else{setSfPresets([]);}}).catch(()=>{setSfPresets([]);});},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectedSoundFontId===sfId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},sfName);}),// ── VST Instruments (scanned bởi Plugin Manager) ── -instrumentSelectorData?.vst_instruments?.length>0&&/*#__PURE__*/React.createElement("div",{className:"mt-3 text-[10px] text-zinc-500 uppercase font-bold px-1 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3 text-violet-400"}),"VST Instruments"),(instrumentSelectorData?.vst_instruments||[]).map(v=>/*#__PURE__*/React.createElement("button",{key:'vst_modal_'+(v.id||v.name),onClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,v.id,v.name||v.id);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-cyan-400 shrink-0"},v.type||"VST")))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto space-y-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),sfPresets===null?(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},"Loading instruments...")):sfPresets.length>0?sfPresets.filter(p=>!selectedSoundFontId||p._sfId===selectedSoundFontId).filter(p=>!sfPresetSearchQuery||(p.name||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())||(p._sfName||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:i,onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program),onDoubleClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800/60 hover:bg-amber-800 text-zinc-300 truncate flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 shrink-0"},p._sfDisplay),p.bank===128?/*#__PURE__*/React.createElement("span",{className:"mr-1"},"🥁"):null,p.name||'Preset '+p.program)):(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},sfPresetSearchQuery?"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 ph\u00f9 h\u1ee3p.":"No presets found.")))))),fxSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:()=>setFxSelectorTrackId(null)},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-64 p-4 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400 mb-3"},"Track FX"),/*#__PURE__*/React.createElement("div",{className:"space-y-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'chorus'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300"},"Chorus"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'reverb'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300"},"Reverb")))),instrumentDropdownTrackId&&instrumentDropdownBtnRect&&/*#__PURE__*/React.createElement("div",{"data-instr-dropdown":"",className:"fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",style:{top:instrumentDropdownBtnRect.bottom+4,left:Math.max(4,Math.min(instrumentDropdownBtnRect.left,window.innerWidth-224))}},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:instrumentSearchQuery,onChange:e=>setInstrumentSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs outline-none"}),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentDropdownTrackId,null),className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local?/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);window.SonicAPI.openInCarla().then(function(r){if(r&&r.success){showToast('Đã mở Carla — chọn VSTi, chỉnh âm, Save preset (.vstpreset) rồi Upload trong app','success');}}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 font-semibold flex items-center justify-between",title:"Mở Carla.exe trên hệ thống (native GUI VSTi) — không cần scan VST trong app"},"\uD83C\uDF9B Carla Bridge (m\u1EDF Carla.exe)",/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-teal-500 shrink-0 ml-1"},"GUI")):null,filteredInstruments.soundfonts.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"SoundFonts"),filteredInstruments.soundfonts.map((sf,i)=>/*#__PURE__*/React.createElement("button",{key:"sfd_"+i,onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,"sf_"+sf.id,sf.display||sf.name||sf.id,undefined,undefined);},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},sf.display||sf.name||sf.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-amber-400 shrink-0 ml-1"},"SF"))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Instruments"),filteredInstruments.vst.map((v,i)=>/*#__PURE__*/React.createElement("div",{key:"vstd_"+i,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,v.id,v.name||v.id);},className:"flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-cyan-400 shrink-0 ml-1"},v.type||"VST")),window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local?/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();window.SonicAPI.openInCarla(v.id).then(function(r){if(r&&r.success){showToast('Đã mở Carla: '+(v.name||v.id),'success');}}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});},className:"shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",title:"Mở trong Carla (native GUI)"},"\uD83C\uDF9B"):null)),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Presets (.vstpreset)"),(window.SonicRuntime&&window.SonicRuntime.presets||[]).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:"psd_"+i,onClick:()=>setTrackPreset(instrumentDropdownTrackId,p.id),className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name||p.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-emerald-400 shrink-0 ml-1"},"PRESET"))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;window.SonicAPI.uploadPreset(f).then(function(r){if(r&&r.success){if(window.SonicRuntime)window.SonicRuntime.refreshPresets();showToast('Đã upload preset: '+(r.original_name||r.name||''),'success');}else{showToast('Upload preset thất bại','error');}}).catch(function(err){showToast('Upload lỗi: '+(err.message||err),'error');});};inp.click();},className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",title:"Upload preset .vstpreset xuất từ Carla (native GUI)"},"\u2B06 Upload preset (t\u1EEB Carla...)"),!filteredInstruments.soundfonts.length&&!filteredInstruments.vst.length&&/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-500 py-4 text-center"},"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o"))));};const root=ReactDOM.createRoot(document.getElementById('root'));root.render(/*#__PURE__*/React.createElement(App,null));setTimeout(()=>lucide.createIcons(),300); \ No newline at end of file +instrumentSelectorData?.vst_instruments?.length>0&&/*#__PURE__*/React.createElement("div",{className:"mt-3 text-[10px] text-zinc-500 uppercase font-bold px-1 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3 text-violet-400"}),"VST Instruments"),(instrumentSelectorData?.vst_instruments||[]).map(v=>/*#__PURE__*/React.createElement("button",{key:'vst_modal_'+(v.id||v.name),onClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,v.id,v.name||v.id);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-cyan-400 shrink-0"},v.type||"VST")))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto space-y-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),sfPresets===null?(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},"Loading instruments...")):sfPresets.length>0?sfPresets.filter(p=>!selectedSoundFontId||p._sfId===selectedSoundFontId).filter(p=>!sfPresetSearchQuery||(p.name||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())||(p._sfName||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:i,onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program),onDoubleClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800/60 hover:bg-amber-800 text-zinc-300 truncate flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 shrink-0"},p._sfDisplay),p.bank===128?/*#__PURE__*/React.createElement("span",{className:"mr-1"},"🥁"):null,p.name||'Preset '+p.program)):(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},sfPresetSearchQuery?"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 ph\u00f9 h\u1ee3p.":"No presets found.")))))),fxSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:()=>setFxSelectorTrackId(null)},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-64 p-4 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400 mb-3"},"Track FX"),/*#__PURE__*/React.createElement("div",{className:"space-y-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'chorus'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300"},"Chorus"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'reverb'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300"},"Reverb")))),instrumentDropdownTrackId&&instrumentDropdownBtnRect&&/*#__PURE__*/React.createElement("div",{"data-instr-dropdown":"",className:"fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",style:{top:instrumentDropdownBtnRect.bottom+4,left:Math.max(4,Math.min(instrumentDropdownBtnRect.left,window.innerWidth-224))}},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:instrumentSearchQuery,onChange:e=>setInstrumentSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs outline-none"}),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentDropdownTrackId,null),className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local?/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);window.SonicAPI.openInCarla().then(function(r){if(r&&r.success){showToast('Đã mở Carla — chọn VSTi, chỉnh âm, Save preset (.vstpreset) rồi Upload trong app','success');}}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 font-semibold flex items-center justify-between",title:"Mở Carla.exe trên hệ thống (native GUI VSTi) — không cần scan VST trong app"},"\uD83C\uDF9B Carla Bridge (m\u1EDF Carla.exe)",/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-teal-500 shrink-0 ml-1"},"GUI")):null,filteredInstruments.soundfonts.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"SoundFonts"),filteredInstruments.soundfonts.map((sf,i)=>/*#__PURE__*/React.createElement("button",{key:"sfd_"+i,onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,"sf_"+sf.id,sf.display||sf.name||sf.id,undefined,undefined);},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},sf.display||sf.name||sf.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-amber-400 shrink-0 ml-1"},"SF"))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Instruments"),filteredInstruments.vst.map((v,i)=>/*#__PURE__*/React.createElement("div",{key:"vstd_"+i,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,v.id,v.name||v.id);// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) — +// Carla load sẵn plugin, native GUI + keyboard ảo để preview realtime. +if(window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local){window.SonicAPI.openInCarla(v.id).then(function(r){if(r&&r.success)showToast('Đã mở Carla với '+(v.name||v.id)+' — chọn preset, bấm keyboard để preview','success');}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});}},className:"flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-cyan-400 shrink-0 ml-1"},v.type||"VST")),window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.features&&window.SonicRuntime.capabilities.features.carla_local?/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();window.SonicAPI.openInCarla(v.id).then(function(r){if(r&&r.success){showToast('Đã mở Carla: '+(v.name||v.id),'success');}}).catch(function(err){showToast('Lỗi mở Carla: '+(err.message||err),'error');});},className:"shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",title:"Mở trong Carla (native GUI)"},"\uD83C\uDF9B"):null)),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Presets (.vstpreset)"),(window.SonicRuntime&&window.SonicRuntime.presets||[]).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:"psd_"+i,onClick:()=>setTrackPreset(instrumentDropdownTrackId,p.id),className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name||p.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-emerald-400 shrink-0 ml-1"},"PRESET"))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;window.SonicAPI.uploadPreset(f).then(function(r){if(r&&r.success){if(window.SonicRuntime)window.SonicRuntime.refreshPresets();showToast('Đã upload preset: '+(r.original_name||r.name||''),'success');}else{showToast('Upload preset thất bại','error');}}).catch(function(err){showToast('Upload lỗi: '+(err.message||err),'error');});};inp.click();},className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",title:"Upload preset .vstpreset xuất từ Carla (native GUI)"},"\u2B06 Upload preset (t\u1EEB Carla...)"),!filteredInstruments.soundfonts.length&&!filteredInstruments.vst.length&&/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-500 py-4 text-center"},"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o"))));};const root=ReactDOM.createRoot(document.getElementById('root'));root.render(/*#__PURE__*/React.createElement(App,null));setTimeout(()=>lucide.createIcons(),300); \ No newline at end of file diff --git a/md/52_CARLA_BRIDGE.md b/md/52_CARLA_BRIDGE.md index 4cd9fba..87a5aea 100644 --- a/md/52_CARLA_BRIDGE.md +++ b/md/52_CARLA_BRIDGE.md @@ -93,11 +93,13 @@ xem §8). 1. Tải: (bản `win64`). 2. Giải nén ra bất kỳ đâu (VD `D:\Tools\Carla\`, chứa `carla.exe`). -3. Mở app → **Plugin Manager** → section **"Carla Bridge (VSTi native GUI)"** → - nút **"Định vị Carla..."** → chọn thư mục chứa `carla.exe` (hoặc chính file - `carla.exe`). +3. Khai báo 1 lần (2 cách, tùy chọn 1): + - **Nút bấm**: Plugin Manager → section **"Carla Bridge (VSTi native GUI)"** → + **"Định vị Carla..."** → chọn thư mục chứa `carla.exe` (hoặc chính file). + - **Nhập tay**: ô text trong section đó → nhập `D:/Tools/Carla` → **Lưu**. 4. App lưu vào `storage/carla_path.json` → cache detect bị xóa → nút - **"Carla Bridge"** hiện trong dropdown nút Synth. + **"Carla Bridge"** hiện trong dropdown nút Synth + **tự động mở Carla** + khi chọn VSTi. ### 3.1 Thứ tự phát hiện `carla_local` @@ -119,10 +121,19 @@ body: { "carla_path": "D:/Tools/Carla" } (thư mục HOẶC file exe) ## 4. Luồng sử dụng end-to-end (Windows) -1. **Nút Synth** (track strip / btnSynth) → chọn **"🎛 Carla Bridge (mở Carla.exe)"** - → app spawn `carla.exe` (ưu tiên `carla-single ` nếu chọn kèm plugin). -2. Trong Carla: **Add Plugin** → chọn VSTi (Kontakt, Nexus, Vital...) → native GUI - hiện ra → chỉnh âm, chọn bank/preset của plugin. +1. **Nút Synth** → chọn **VSTi** trong danh sách → app **TỰ ĐỘNG gọi Carla**: + sinh file project `.carxs` (định dạng XML chính thức của Carla — + `carla.exe [FILE]` nhận project file) chứa node + `VST3path...` + → `carla.exe ` → Carla mở lên **plugin đã load sẵn** kèm + **on-screen MIDI keyboard** (`PixmapKeyboard`). + - VST3 (file `.vst3` hoặc folder `X.vst3` Windows): auto-load qua `.carxs`. + - VST2 (`.dll`/`.so` ngoài `.vst3`): không auto-load tin cậy (cần uniqueID) + → mở Carla trống để user **Add Plugin** thủ công. + - Project `.carxs` nằm `{STORAGE_DIR}/carla_projects/`, tự dọn sau 1 ngày. +2. Trong Carla: native GUI của VSTi hiện ra → **chọn instrument/preset** của + plugin → **bật ARM** trên track trong app (tùy chọn) → **bấm phím trên + keyboard ảo của Carla** để preview realtime (âm thật qua audio device). 3. **Save preset** bằng nút của CHÍNH plugin (không dùng project save của Carla) → file `.vstpreset`. 4. Trong app: dropdown Synth → **"⬆ Upload preset (từ Carla...)"** → chọn file diff --git a/wiki.md b/wiki.md index 2925f7d..d839e94 100644 --- a/wiki.md +++ b/wiki.md @@ -3036,3 +3036,8 @@ - **Tóm tắt thay đổi:** (1) Runtime tự phát hiện môi trường `app/core/runtime.py` (desktop Windows / docker headless; override `SF_RUNTIME`/`SF_DOCKER`) + `GET /api/v1/system/capabilities` (public) — frontend bật/tắt tính năng theo môi trường. (2) Carla Bridge: mục "🎛 Carla Bridge (mở Carla.exe)" trong dropdown nút Synth (chỉ hiện khi desktop + có Carla local) → spawn `carla.exe` qua `POST /api/v1/plugins/open-in-carla`; vì bản Windows là zip portable (không installer, không PATH) → Plugin Manager thêm section "Carla Bridge" + nút "Định vị Carla..." (`POST /api/v1/system/carla-path`, lưu `storage/carla_path.json` ưu tiên cao nhất; kèm registry + quét nông Downloads/Desktop/Documents giới hạn độ sâu). (3) Thư viện preset `app/api/v1/presets.py` (storage/presets: list/upload/download/delete, chống path traversal) + `apply_preset_to_plugin()` trong `vst_engine.py` (preset_data base64 → preset_id → preset_path) → render_engine nạp preset trước khi render VST3 → âm render = âm đã chỉnh trong Carla. (4) `POST /api/v1/plugins/preview` — quick-render preview (cùng code path pedalboard với export → âm thật). (5) Plugin Manager: bấm soundfont expand → liệt kê instrument (bank/program/name) + nút "Chèn vào Synth" gán vào track đang chọn. (6) `config.py` default VST_DIR/SOUNDFONT_DIR theo platform + `PRESET_DIR`; docker-compose `SF_DOCKER=1`; service `runtime.js` (capabilities lúc boot, cache preset) + api.js methods mới (getCapabilities/setCarlaPath/openInCarla/previewInstrument/listPresets/uploadPreset/deletePreset). - **Các file ảnh hưởng:** `app/core/runtime.py` (mới), `app/api/v1/system.py` (mới), `app/api/v1/presets.py` (mới), `app/api/v1/plugins.py`, `app/core/vst_engine.py`, `app/core/render_engine.py`, `app/config.py`, `app/main.py`, `app/static/js/services/runtime.js` (mới), `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html`, `.env.example`, `docker-compose.prod.yml`, `md/52_CARLA_BRIDGE.md` (mới), `wiki.md` - **Ghi chú/Test (nếu có):** `pytest`: 86 passed, 7 skipped. Test API: capabilities 200; preset CRUD + chặn path traversal; carla-path (thư mục/exe → resolve exe, invalid → 400); open-in-carla → 409 kèm hướng dẫn khi chưa có Carla; preview → 501 khi thiếu pedalboard. Rebuild bundle BUILD OK. Lưu ý: pedalboard 0.10+ đã bỏ VST2 (chỉ preset VST3 `.vstpreset` round-trip); render cùng sample rate với Carla để preview = export; Carla GPL-2.0+ → không bundle/nhúng, chỉ spawn tiến trình ngoài. + +### [2026-08-09] Task: Carla Bridge — auto-load VSTi qua .carxs + khai báo thư mục Carla +- **Tóm tắt thay đổi:** (1) `POST /api/v1/plugins/open-in-carla` viết lại: sinh file project `.carxs` (định dạng XML chính thức từ source Carla — `carla.exe [FILE]` nhận project file) chứa `VST3path...` → Carla mở lên **plugin đã load sẵn** kèm on-screen MIDI keyboard (PixmapKeyboard) để preview realtime. VST3 (file `.vst3`/folder `X.vst3` Windows) auto-load; VST2 (`.dll`/`.so` ngoài `.vst3`) → Carla trống để Add Plugin thủ công (cần uniqueID). Project lưu `{STORAGE_DIR}/carla_projects/`, tự dọn sau 1 ngày. (2) Frontend: chọn VSTi trong dropdown Synth → **tự động** `openInCarla(v.id)` (chỉ khi desktop + carla_local) — nhấn nút Synth, thêm VSTi là Carla tự gọi và load luôn VSTi đó. (3) Plugin Manager section "Carla Bridge": thêm **ô nhập tay thư mục chứa carla.exe + nút Lưu** (bên cạnh "Định vị Carla..." dùng picker) — khai báo 1 lần là xong. +- **Các file ảnh hưởng:** `app/api/v1/plugins.py` (+ `_write_carla_project`), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `md/52_CARLA_BRIDGE.md`, `wiki.md` +- **Ghi chú/Test (nếu có):** `pytest`: 86 passed, 7 skipped. Test `_write_carla_project`: VST3 file → carxs hợp lệ (CARLA-PROJECT VERSION='2.5', Type VST3, Binary, Active Yes, ControlChannel 1); Windows VST3 folder → OK; VST2 .dll → '' (không sinh); XML escape tên đặc biệt (A&B ) parse OK. End-to-end endpoint với Carla giả: 200, project_file sinh + chứa Binary, cmd = [carla.exe, carxs]. Rebuild bundle BUILD OK (node qua PATH nvm v24).