diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 0637d37..839d596 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -5299,9 +5299,39 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => { setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 50); } }, [isOpen]); - // Folder picker: Tauri v2 dialog qua invoke (window.__TAURI__.dialog KHONG - // ton tai trong v2 — plugin dialog chi co JS binding qua npm; goi truc tiep - // command 'plugin:dialog|open'). Fallback: prompt nhap path (browser). + // 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 + // khong hoat dong trong WebView2) -> mo TRINH DUYET THU MUC in-app + // (backend /media/computer + /media/browse) — hoat dong moi OS. + // 3) Cuoi cung: prompt nhap path (browser thuan). + const [pmPicker, setPmPicker] = React.useState(null); // {path, dirs, parent, roots, loading} + const openPluginPicker = async () => { + setPmPicker({ path: null, dirs: null, parent: null, roots: null, loading: true }); + try { + const data = await window.SonicAPI.browseComputer(); + setPmPicker({ path: null, dirs: null, parent: null, roots: data.roots || [], loading: false }); + } catch (e) { + setPmPicker(null); + showToast('Không mở được trình duyệt thư mục: ' + (e.message || e), 'error'); + } + }; + const browsePluginDir = async (path) => { + setPmPicker(prev => ({ ...prev, loading: true })); + try { + const data = await window.SonicAPI.browseDir(path); + setPmPicker({ path: data.path, parent: data.parent, dirs: data.dirs || [], roots: null, loading: false }); + } catch (e) { + setPmPicker(prev => ({ ...prev, loading: false })); + showToast('Không đọc được thư mục: ' + (e.message || e), 'error'); + } + }; + const confirmPluginDir = () => { + const p = pmPicker && pmPicker.path; + if (p && !pmDirs.includes(p)) setPmDirs(prev => [...prev, p]); + setPmPicker(null); + }; const pickPluginFolder = async () => { try { if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) { @@ -5313,13 +5343,21 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => { } return; } - const manual = window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):'); - if (manual && manual.trim()) { - const p = manual.trim(); - if (!pmDirs.includes(p)) setPmDirs(prev => [...prev, p]); - } + // Trinh duyet thu muc in-app (backend) — hoat dong khi UI chay tren + // localhost:8000 (khong co __TAURI__, window.prompt vo hieu trong WebView2) + await openPluginPicker(); + return; } catch (e) { - showToast('Browse failed: ' + (e.message || e), 'error'); + // Fallback cuoi: prompt nhap tay (browser thuan, khong phai WebView2) + try { + const manual = window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):'); + if (manual && manual.trim()) { + const p = manual.trim(); + if (!pmDirs.includes(p)) setPmDirs(prev => [...prev, p]); + } + } catch (e2) { + showToast('Browse failed: ' + (e.message || e), 'error'); + } } }; const removePluginDir = (dir) => { @@ -5372,10 +5410,78 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => { 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', + 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 React.createElement('div', { className: 'flex items-center justify-between px-5 py-3 bg-[#252525] border-b border-[#383838]' diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 4d9290a..df3a0c3 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -363,12 +363,21 @@ const GraphEditorCanvas=({buffer,zoom,timelineWidth,volumeNodes,panningNodes,fad useEffect(()=>{if(!isOpen)return;if(window.SonicAPI&&window.SonicAPI.apiRequest){window.SonicAPI.apiRequest('/api/v1/auth/first-time',{method:'GET'}).then(function(d){setFirstTime(!!(d&&d.first_time));})// Fail-closed: endpoint lỗi/404 (backend chưa restart) → ẨN gợi ý // (không hiện — user đã yêu cầu bỏ gợi ý sau lần đầu). .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})=>{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 } -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: Tauri v2 dialog qua invoke (window.__TAURI__.dialog KHONG -// ton tai trong v2 — plugin dialog chi co JS binding qua npm; goi truc tiep -// command 'plugin:dialog|open'). Fallback: prompt nhap path (browser). -const pickPluginFolder=async()=>{try{if(window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke){const sel=await window.__TAURI__.core.invoke('plugin:dialog|open',{options:{directory:true,multiple:false}});if(typeof sel==='string'&&sel){if(!pmDirs.includes(sel))setPmDirs(prev=>[...prev,sel]);}return;}const manual=window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):');if(manual&&manual.trim()){const p=manual.trim();if(!pmDirs.includes(p))setPmDirs(prev=>[...prev,p]);}}catch(e){showToast('Browse failed: '+(e.message||e),'error');}};const removePluginDir=dir=>{setPmDirs(prev=>prev.filter(d=>d!==dir));};// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. +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 +// khong hoat dong trong WebView2) -> mo TRINH DUYET THU MUC in-app +// (backend /media/computer + /media/browse) — hoat dong moi OS. +// 3) Cuoi cung: prompt nhap path (browser thuan). +const[pmPicker,setPmPicker]=React.useState(null);// {path, dirs, parent, roots, loading} +const openPluginPicker=async()=>{setPmPicker({path:null,dirs:null,parent:null,roots:null,loading:true});try{const data=await window.SonicAPI.browseComputer();setPmPicker({path:null,dirs:null,parent:null,roots:data.roots||[],loading:false});}catch(e){setPmPicker(null);showToast('Không mở được trình duyệt thư mục: '+(e.message||e),'error');}};const browsePluginDir=async path=>{setPmPicker(prev=>({...prev,loading:true}));try{const data=await window.SonicAPI.browseDir(path);setPmPicker({path:data.path,parent:data.parent,dirs:data.dirs||[],roots:null,loading:false});}catch(e){setPmPicker(prev=>({...prev,loading:false}));showToast('Không đọc được thư mục: '+(e.message||e),'error');}};const confirmPluginDir=()=>{const p=pmPicker&&pmPicker.path;if(p&&!pmDirs.includes(p))setPmDirs(prev=>[...prev,p]);setPmPicker(null);};const pickPluginFolder=async()=>{try{if(window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke){const sel=await window.__TAURI__.core.invoke('plugin:dialog|open',{options:{directory:true,multiple:false}});if(typeof sel==='string'&&sel){if(!pmDirs.includes(sel))setPmDirs(prev=>[...prev,sel]);}return;}// Trinh duyet thu muc in-app (backend) — hoat dong khi UI chay tren +// localhost:8000 (khong co __TAURI__, window.prompt vo hieu trong WebView2) +await openPluginPicker();return;}catch(e){// Fallback cuoi: prompt nhap tay (browser thuan, khong phai WebView2) +try{const manual=window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):');if(manual&&manual.trim()){const p=manual.trim();if(!pmDirs.includes(p))setPmDirs(prev=>[...prev,p]);}}catch(e2){showToast('Browse failed: '+(e.message||e),'error');}}};const removePluginDir=dir=>{setPmDirs(prev=>prev.filter(d=>d!==dir));};// 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',style:{maxHeight:'80vh'},onClick:e=>e.stopPropagation()},// Header +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 React.createElement('div',{className:'flex items-center justify-between px-5 py-3 bg-[#252525] border-b border-[#383838]'},React.createElement('h3',{className:'text-base font-bold text-cyan-400 flex items-center gap-2'},React.createElement('i',{'data-lucide':'zap',className:'w-4 h-4'}),'Plugin Manager (SoundFont / VSTi)'),React.createElement('button',{onClick:onClose,className:'text-zinc-500 hover:text-zinc-200 transition'},React.createElement('i',{'data-lucide':'x',className:'w-4 h-4'}))),// Left-right body 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 diff --git a/app/static/js/services/api.js b/app/static/js/services/api.js index 4fb0854..5a31a4b 100644 --- a/app/static/js/services/api.js +++ b/app/static/js/services/api.js @@ -67,6 +67,10 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin; getPluginDirs: () => apiRequest('/api/v1/plugins/dirs', { method: 'GET' }), savePluginDirs: (dirs) => apiRequest('/api/v1/plugins/dirs', { method: 'POST', body: JSON.stringify(dirs) }), scanPluginDirs: () => apiRequest('/api/v1/plugins/scan', { method: 'POST' }), + // Folder picker cho Plugin Manager: duyet thu muc qua backend (media) + // — hoat dong moi OS, khong can window.__TAURI__ (UI chay tren localhost:8000) + browseComputer: () => apiRequest('/api/v1/media/computer', { method: 'GET' }), + browseDir: (path) => apiRequest(`/api/v1/media/browse?path=${encodeURIComponent(path)}`, { method: 'GET' }), getSoundfontCatalog: () => apiRequest('/api/v1/plugins/soundfonts/catalog', { method: 'GET' }), listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }), listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }), diff --git a/app/templates/index.html b/app/templates/index.html index d6add09..e4ca882 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -31,7 +31,7 @@ - + @@ -43,7 +43,7 @@ - +