FIX: Carla sử dụng bridge

This commit is contained in:
2026-08-10 07:57:26 +07:00
parent fbaac1f673
commit aa32272f36
17 changed files with 1372 additions and 34 deletions
+201 -22
View File
@@ -5279,7 +5279,7 @@ const AIConfigModal = ({
}, loading ? 'Đang lưu...' : 'Lưu Cấu Hình AI'))))));
};
const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }) => {
if (!isOpen) return null;
const [localData, setLocalData] = React.useState(pluginsData);
const [sfUploadStatus, setSfUploadStatus] = React.useState('');
@@ -5288,6 +5288,12 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
const [pmScanning, setPmScanning] = React.useState(false);
const [pmScanResult, setPmScanResult] = React.useState('');
const [pmScanData, setPmScanData] = React.useState(null); // { vst_found, soundfonts }
// Instrument bên trong mi 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()
@@ -5365,6 +5371,51 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
const removePluginDir = (dir) => {
setPmDirs(prev => prev.filter(d => d !== dir));
};
// Expand 1 soundfont đc danh sách instrument (bank/program/name) bên trong
// qua API soundfont-instruments/{id} hin th nút "Chèn vào Synth".
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 bn 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 thy user t chn thư mc
// cha 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ư mc refresh list + catalog.
const saveAndScanDirs = async () => {
setPmScanning(true);
@@ -5545,28 +5596,78 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
) :
(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) =>
React.createElement('div', {
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: 'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-amber-800/50 transition group'
className: 'bg-[#252525] rounded-lg border border-[#333] hover:border-amber-800/50 transition group'
},
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', {
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', 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)
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')
)
),
React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('button', {
onClick: () => 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)
React.createElement('div', {
@@ -14279,6 +14380,11 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
<div className={`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst ? 'bg-slate-200' : ''}`} onClick={() => selectSynthInst(null)}>
<i className="fa-solid fa-ban text-slate-400"></i> None (mặc định)
</div>
{window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local && (
<div className="flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-teal-100 font-semibold text-teal-700 border-t border-[#e0e0e0]" onClick={() => { setSynthOpen(false); window.SonicAPI.openInCarla().then(r => { if (r && r.success) showToast('Đã mở Carla — chỉnh preset rồi Upload trong app', 'success'); }).catch(err => showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }}>
<i className="fa-solid fa-sliders text-teal-500"></i> 🎛 Carla Bridge (mở Carla.exe)
</div>
)}
{!synthLoading && filteredSynthList && filteredSynthList.map(group => (
<div key={group.sf.id || group.sf.name}>
<div className="px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate">{group.sf.display || group.sf.name || group.sf.id}</div>
@@ -15520,6 +15626,27 @@ const App = () => {
setAiPrompt(newText);
};
// Gán preset VST3 (.vstpreset t thư vin storage/presets cu ni
// Carla pedalboard) vào track: preset_id nm trong synth_engine render
// engine ti qua load_preset khi render âm render = âm đã chnh trong Carla.
const setTrackPreset = (trackId, presetId) => {
if (!trackId || !presetId) return;
var mt = activeTracksRef.current || tracks;
var cur = null;
for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { cur = mt[ci]; break; } }
if (!cur) return;
var se = cur.synth_engine || {};
if (!se.plugin_id || String(se.type || '').indexOf('vst') === -1) {
showToast('Chọn VST instrument trước khi gán preset', 'warning');
return;
}
updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, synth_engine: { ...(t.synth_engine || {}), preset_id: presetId } } : t));
setInstrumentDropdownTrackId(null);
setInstrumentDropdownBtnRect(null);
var p = ((window.SonicRuntime && window.SonicRuntime.presets) || []).find(function (x) { return x.id === presetId; });
showToast('Đã gán preset: ' + ((p && p.name) || presetId), 'success');
};
const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber) => {
const track = activeTracks.find(t => t.id === trackId);
if (!track) return;
@@ -29802,7 +29929,16 @@ STRICT CONSTRAINTS:
}), /*#__PURE__*/React.createElement(PluginManagerModal, {
isOpen: pluginManagerModalOpen,
onClose: () => setPluginManagerModalOpen(false),
pluginsData: pluginsData
pluginsData: pluginsData,
onInsertInstrument: (inst) => {
// Chèn instrument (soundfont bank/program) vào track đang chn 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); }
@@ -29943,6 +30079,14 @@ STRICT CONSTRAINTS:
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,
@@ -29950,11 +30094,46 @@ STRICT CONSTRAINTS:
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("button", {
filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("div", {
key: "vstd_" + i,
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
className: "w-full 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"))),
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")
)));
};
File diff suppressed because one or more lines are too long
+20
View File
@@ -17,6 +17,8 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
async function apiRequest(endpoint, options = {}) {
const url = `${window.API_BASE_URL}${endpoint}`;
const headers = { ...getAuthHeaders(), ...options.headers };
// FormData: browser tự đặt Content-Type kèm boundary — không được ép JSON
if (options.body instanceof FormData) delete headers['Content-Type'];
const response = await fetch(url, { ...options, headers });
if (response.status === 401) {
@@ -67,6 +69,24 @@ 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' }),
// Runtime capabilities — frontend gọi lúc boot để biết môi trường
// (desktop Windows / docker headless) và bật/tắt tính năng
getCapabilities: () => apiRequest('/api/v1/system/capabilities', { method: 'GET' }),
// Định vị Carla.exe (bản portable zip không cài đặt/PATH) — lưu config
setCarlaPath: (path) => apiRequest('/api/v1/system/carla-path', { method: 'POST', body: JSON.stringify({ carla_path: path }) }),
// Mở native GUI VSTi trong Carla (chỉ khi runtime=desktop + có Carla local)
openInCarla: (pluginName, pluginPath) => apiRequest('/api/v1/plugins/open-in-carla', { method: 'POST', body: JSON.stringify({ plugin_name: pluginName, plugin_path: pluginPath }) }),
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
uploadPreset: (file, pluginHint) => {
const fd = new FormData();
fd.append('file', file);
if (pluginHint) fd.append('plugin_hint', pluginHint);
return apiRequest('/api/v1/presets/upload', { method: 'POST', body: fd });
},
deletePreset: (presetId) => apiRequest(`/api/v1/presets/${presetId}`, { method: 'DELETE' }),
// Native folder picker (Explorer qua Tauri bridge / PowerShell) —
// user yêu cầu dùng window explorer, không nhập tay
pickPluginDir: () => apiRequest('/api/v1/plugins/pick-dir', { method: 'POST' }),
+62
View File
@@ -0,0 +1,62 @@
// SonicForge Runtime service — phát hiện môi trường chạy (desktop Windows /
// docker headless) qua /api/v1/system/capabilities, bật/tắt tính năng theo đó.
// - data-runtime trên <html>: "desktop" | "headless"
// - data-carla="1": có Carla local (hiện nút "Mở trong Carla")
// - Phần tử có thuộc tính data-carla-only sẽ bị ẩn khi không có Carla local.
// - Thư viện preset (.vstpreset) cache trong SonicRuntime.presets — dùng cho
// dropdown gán preset vào track (Carla → pedalboard bridge).
window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null, presets: null };
(function () {
function getHeaders() {
const token = localStorage.getItem('sonic_token') || '';
return token ? { 'Authorization': 'Bearer ' + token } : {};
}
function load() {
return fetch(window.API_BASE_URL + '/api/v1/system/capabilities')
.then(function (r) { return r.json(); })
.then(function (data) {
var c = data && data.success ? data : { features: {} };
window.SonicRuntime.capabilities = c;
window.SonicRuntime.loaded = true;
var html = document.documentElement;
html.dataset.runtime = c.runtime || 'unknown';
html.dataset.platform = c.platform || '';
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
if (c.features && c.features.carla_local === false) {
document.querySelectorAll('[data-carla-only]').forEach(function (el) {
el.style.display = 'none';
});
}
// Cache sẵn danh sách preset (static, ít thay đổi)
listPresets().catch(function () {});
return c;
})
.catch(function () { return null; });
}
function listPresets() {
if (window.SonicRuntime.presets) return Promise.resolve(window.SonicRuntime.presets);
return fetch(window.API_BASE_URL + '/api/v1/presets', { headers: getHeaders() })
.then(function (r) { return r.json(); })
.then(function (d) {
window.SonicRuntime.presets = (d && d.presets) || [];
return window.SonicRuntime.presets;
})
.catch(function () { return []; });
}
window.SonicRuntime.load = load;
window.SonicRuntime.listPresets = listPresets;
window.SonicRuntime.refreshPresets = function () {
window.SonicRuntime.presets = null;
return window.SonicRuntime.listPresets();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', load);
} else {
load();
}
})();