feat: cài đặt tools cho AI thực hiện tạo MIDI
This commit is contained in:
+450
-52
@@ -3160,57 +3160,53 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
};
|
||||
const vsts = localData?.vst_instruments || [];
|
||||
const sfs = localData?.soundfonts || [];
|
||||
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-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200',
|
||||
onClick: e => e.stopPropagation()
|
||||
}, React.createElement('div', {
|
||||
className: 'flex justify-between items-center pb-4 border-b border-[#383838]'
|
||||
}, React.createElement('h3', {
|
||||
className: 'text-lg font-bold text-cyan-400'
|
||||
}, 'Plugin Manager (SoundFont / VSTi)'), React.createElement('button', {
|
||||
onClick: onClose,
|
||||
className: 'text-slate-400 hover:text-slate-200'
|
||||
}, '✕')), React.createElement('div', {
|
||||
className: 'mt-4 space-y-4'
|
||||
}, React.createElement('div', null, React.createElement('h4', {
|
||||
className: 'text-sm font-bold text-purple-400 mb-2 uppercase'
|
||||
}, 'VST Instruments'), vsts.length === 0 ? React.createElement('p', {
|
||||
className: 'text-xs text-zinc-500'
|
||||
}, 'No VST instruments available on server.') : React.createElement('div', {
|
||||
className: 'grid grid-cols-1 gap-1'
|
||||
}, vsts.map((v, i) => React.createElement('div', {
|
||||
key: i,
|
||||
className: 'flex items-center justify-between bg-[#1e1e1e] px-3 py-2 rounded border border-[#333]'
|
||||
}, React.createElement('span', {
|
||||
className: 'text-xs font-semibold text-slate-200'
|
||||
}, v.id), React.createElement('span', {
|
||||
className: 'text-[10px] text-cyan-400 bg-cyan-950/40 px-1.5 py-0.5 rounded'
|
||||
}, v.type))))), React.createElement('div', null, React.createElement('h4', {
|
||||
className: 'text-sm font-bold text-amber-400 mb-2 uppercase'
|
||||
}, 'SoundFonts'), sfs.length === 0 ? React.createElement('p', {
|
||||
className: 'text-xs text-zinc-500'
|
||||
}, 'No SoundFonts available.') : React.createElement('div', {
|
||||
className: 'space-y-1'
|
||||
}, sfs.map((sf, i) => React.createElement('div', {
|
||||
key: i,
|
||||
className: 'flex items-center justify-between bg-[#1e1e1e] px-3 py-2 rounded border border-[#333]'
|
||||
}, React.createElement('span', {
|
||||
className: 'text-xs text-slate-200'
|
||||
}, sf.name))))), React.createElement('div', {
|
||||
className: 'pt-4 border-t border-[#383838]'
|
||||
}, React.createElement('h4', {
|
||||
className: 'text-sm font-bold text-emerald-400 mb-2 uppercase'
|
||||
}, 'Upload SoundFont'), React.createElement('input', {
|
||||
type: 'file',
|
||||
accept: '.sf2,.sf3',
|
||||
onChange: handleUploadSF,
|
||||
className: 'w-full text-xs text-zinc-400 file:mr-2 file:py-1 file:px-3 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-emerald-700 file:text-white hover:file:bg-emerald-600'
|
||||
}), sfUploadStatus && React.createElement('p', {
|
||||
className: 'text-xs mt-1 text-zinc-400'
|
||||
}, sfUploadStatus))))));
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className="bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex justify-between items-center pb-4 border-b border-[#383838]">
|
||||
<h3 className="text-lg font-bold text-cyan-400">Plugin Manager (SoundFont / VSTi)</h3>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-slate-200">✕</button>
|
||||
</div>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-purple-400 mb-2 uppercase">VST Instruments</h4>
|
||||
{vsts.length === 0 ? (
|
||||
<p className="text-xs text-zinc-500">No VST instruments available on server.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-1">
|
||||
{vsts.map((v, i) => (
|
||||
<div key={i} className="flex items-center justify-between bg-[#1e1e1e] px-3 py-2 rounded border border-[#333]">
|
||||
<span className="text-xs font-semibold text-slate-200">{v.id}</span>
|
||||
<span className="text-[10px] text-cyan-400 bg-cyan-950/40 px-1.5 py-0.5 rounded">{v.type}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-amber-400 mb-2 uppercase">SoundFonts</h4>
|
||||
{sfs.length === 0 ? (
|
||||
<p className="text-xs text-zinc-500">No SoundFonts available.</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{sfs.map((sf, i) => (
|
||||
<div key={i} className="flex items-center justify-between bg-[#1e1e1e] px-3 py-2 rounded border border-[#333]">
|
||||
<span className="text-xs text-slate-200">{sf.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4 border-t border-[#383838]">
|
||||
<h4 className="text-sm font-bold text-emerald-400 mb-2 uppercase">Upload SoundFont</h4>
|
||||
<input type="file" accept=".sf2,.sf3" onChange={handleUploadSF}
|
||||
className="w-full text-xs text-zinc-400 file:mr-2 file:py-1 file:px-3 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-emerald-700 file:text-white hover:file:bg-emerald-600" />
|
||||
{sfUploadStatus && <p className="text-xs mt-1 text-zinc-400">{sfUploadStatus}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileModal = ({
|
||||
@@ -3881,6 +3877,313 @@ const SystemManagerModal = ({
|
||||
className: "px-2 py-1 bg-red-700/60 hover:bg-red-600 rounded text-xs"
|
||||
}, "Xóa")))))))));
|
||||
};
|
||||
|
||||
const DEFAULT_PRESETS = [
|
||||
{
|
||||
id: "preset_epic_orchestra_intro",
|
||||
name: "Epic Orchestra Intro (8 Bars)",
|
||||
keywords: ["epic orchestra", "epic orchestral", "hoành tráng", "nhạc phim epic"],
|
||||
category: "Orchestral / Film Score",
|
||||
default_bars: 8,
|
||||
default_bpm: 130,
|
||||
default_scale: "C Minor",
|
||||
system_instruction_template: "You are a professional Epic Orchestral film composer. Create a powerful, dramatic 8-bar intro composition.\nThe required structure to return via the `generate_multitrack_midi` tool consists of 5 tracks:\n1. Strings Ensemble: Plays staccato 16th notes in the low register (C2, G2) driving the rhythm (Ostinato).\n2. Brass Section: Plays the main swelling melodic theme (Horn/Trumpet swells) in the C3-C5 range.\n3. Epic Percussion / Taiko: Hits heavily on beats 1 and 3, featuring a snare roll accent at bars 4 and 8.\n4. Synth Bass/Pad: Holds smooth octave foundation layers (Legato).\n5. Orchestral Drums/Cymbals: Crashes on bar 1 and bar 5.\nEnsure the duration of each track is precisely 8 bars (32 beats).",
|
||||
is_user_defined: false,
|
||||
created_at: "2026-07-23T16:00:00Z"
|
||||
},
|
||||
{
|
||||
id: "preset_pop_piano_chords",
|
||||
name: "Pop Piano Chords (4 Bars)",
|
||||
keywords: ["pop piano", "piano chords", "ballad piano", "hợp âm piano"],
|
||||
category: "Pop / Ballad",
|
||||
default_bars: 4,
|
||||
default_bpm: 90,
|
||||
default_scale: "C Major",
|
||||
system_instruction_template: "You are a professional Pop Piano player. Generate a beautiful 4-bar piano chord progression (e.g. C - G - Am - F) with pleasant chord voicing and simple accompaniment. Return the MIDI notes via `generate_multitrack_midi` function on a track named 'Pop Piano'. Ensure the duration of the track is precisely 4 bars (16 beats).",
|
||||
is_user_defined: false,
|
||||
created_at: "2026-07-23T16:00:00Z"
|
||||
},
|
||||
{
|
||||
id: "preset_cyberpunk_synth",
|
||||
name: "Cyberpunk Synthwave (8 Bars)",
|
||||
keywords: ["cyberpunk synth", "synthwave", "cyberpunk", "futuristic synth"],
|
||||
category: "Electronic / Synthwave",
|
||||
default_bars: 8,
|
||||
default_bpm: 120,
|
||||
default_scale: "A Minor",
|
||||
system_instruction_template: "You are a Synthwave producer. Generate a driving 8-bar cyberpunk synth bassline and lead melody. Return MIDI notes via `generate_multitrack_midi` containing:\n1. Synth Bass: driving eighth notes on pitch A1, C2, G1.\n2. Synth Lead: arpeggiated melodic line in high register C4-E5.\nEnsure the duration of each track is precisely 8 bars (32 beats).",
|
||||
is_user_defined: false,
|
||||
created_at: "2026-07-23T16:00:00Z"
|
||||
}
|
||||
];
|
||||
|
||||
const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const [presets, setPresets] = React.useState(() => {
|
||||
const local = localStorage.getItem('daw_ai_prompt_presets');
|
||||
return local ? JSON.parse(local) : DEFAULT_PRESETS;
|
||||
});
|
||||
|
||||
const [search, setSearch] = React.useState('');
|
||||
const [filterCategory, setFilterCategory] = React.useState('ALL');
|
||||
const [editingPreset, setEditingPreset] = React.useState(null); // preset object or 'new'
|
||||
|
||||
// Form states
|
||||
const [formName, setFormName] = React.useState('');
|
||||
const [formKeywords, setFormKeywords] = React.useState('');
|
||||
const [formCategory, setFormCategory] = React.useState('Orchestral / Film Score');
|
||||
const [formBars, setFormBars] = React.useState(8);
|
||||
const [formBpm, setFormBpm] = React.useState(120);
|
||||
const [formScale, setFormScale] = React.useState('C Minor');
|
||||
const [formTemplate, setFormTemplate] = React.useState('');
|
||||
|
||||
const savePresets = (newPresets) => {
|
||||
setPresets(newPresets);
|
||||
localStorage.setItem('daw_ai_prompt_presets', JSON.stringify(newPresets));
|
||||
};
|
||||
|
||||
const handleEdit = (p) => {
|
||||
setEditingPreset(p);
|
||||
setFormName(p.name);
|
||||
setFormKeywords(p.keywords.join(', '));
|
||||
setFormCategory(p.category);
|
||||
setFormBars(p.default_bars);
|
||||
setFormBpm(p.default_bpm);
|
||||
setFormScale(p.default_scale);
|
||||
setFormTemplate(p.system_instruction_template);
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
setEditingPreset('new');
|
||||
setFormName('');
|
||||
setFormKeywords('');
|
||||
setFormCategory('Orchestral / Film Score');
|
||||
setFormBars(8);
|
||||
setFormBpm(120);
|
||||
setFormScale('C Minor');
|
||||
setFormTemplate('');
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
const updated = presets.filter(p => p.id !== id);
|
||||
savePresets(updated);
|
||||
showToast('Đã xóa preset.', 'info');
|
||||
};
|
||||
|
||||
const handleSaveForm = (e) => {
|
||||
e.preventDefault();
|
||||
if (!formName.trim() || !formTemplate.trim()) {
|
||||
showToast('Vui lòng điền đầy đủ tên và mẫu gợi ý.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const keywordsArray = formKeywords.split(',').map(k => k.trim()).filter(Boolean);
|
||||
const presetObj = {
|
||||
id: editingPreset === 'new' ? 'preset_' + Date.now() : editingPreset.id,
|
||||
name: formName.trim(),
|
||||
keywords: keywordsArray,
|
||||
category: formCategory,
|
||||
default_bars: parseInt(formBars) || 8,
|
||||
default_bpm: parseInt(formBpm) || 120,
|
||||
default_scale: formScale,
|
||||
system_instruction_template: formTemplate.trim(),
|
||||
is_user_defined: true,
|
||||
created_at: editingPreset === 'new' ? new Date().toISOString() : editingPreset.created_at
|
||||
};
|
||||
|
||||
let updated;
|
||||
if (editingPreset === 'new') {
|
||||
updated = [...presets, presetObj];
|
||||
} else {
|
||||
updated = presets.map(p => p.id === presetObj.id ? presetObj : p);
|
||||
}
|
||||
|
||||
savePresets(updated);
|
||||
setEditingPreset(null);
|
||||
showToast('Đã lưu preset thành công!', 'success');
|
||||
};
|
||||
|
||||
const categories = ['ALL', ...new Set(presets.map(p => p.category))];
|
||||
|
||||
const filtered = presets.filter(p => {
|
||||
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.keywords.some(k => k.toLowerCase().includes(search.toLowerCase()));
|
||||
const matchesCategory = filterCategory === 'ALL' || p.category === filterCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4 animate-fade-in"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-[#18181b] border border-zinc-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden text-zinc-100"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "p-4 border-b border-zinc-800 flex items-center justify-between shrink-0 bg-[#202024]"
|
||||
}, /*#__PURE__*/React.createElement("h2", {
|
||||
className: "text-sm font-bold tracking-wider uppercase text-purple-400 flex items-center gap-1.5"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "sliders",
|
||||
className: "w-4 h-4"
|
||||
}), "AI Prompt Preset Manager"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => { setEditingPreset(null); onClose(); },
|
||||
className: "text-zinc-400 hover:text-zinc-200 transition"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "x",
|
||||
className: "w-4 h-4"
|
||||
}))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 overflow-y-auto p-4 flex gap-4 min-h-0"
|
||||
}, !editingPreset ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 flex flex-col min-w-0"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-2 mb-3 shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
placeholder: "Tìm kiếm preset hoặc từ khóa...",
|
||||
value: search,
|
||||
onChange: e => setSearch(e.target.value),
|
||||
className: "flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"
|
||||
}), /*#__PURE__*/React.createElement("select", {
|
||||
value: filterCategory,
|
||||
onChange: e => setFilterCategory(e.target.value),
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"
|
||||
}, categories.map(c => /*#__PURE__*/React.createElement("option", {
|
||||
key: c,
|
||||
value: c
|
||||
}, c === 'ALL' ? 'Tất cả danh mục' : c))), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: handleNew,
|
||||
className: "px-3 py-1 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold flex items-center gap-1.5 shadow transition shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "plus",
|
||||
className: "w-3.5 h-3.5"
|
||||
}), "Tạo mới")), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 border border-zinc-800 rounded bg-[#0f0f12] overflow-y-auto"
|
||||
}, filtered.length === 0 ? /*#__PURE__*/React.createElement("div", {
|
||||
className: "p-8 text-center text-zinc-500 text-xs italic"
|
||||
}, "Không tìm thấy preset nào.") : /*#__PURE__*/React.createElement("table", {
|
||||
className: "w-full text-left text-xs border-collapse"
|
||||
}, /*#__PURE__*/React.createElement("thead", {
|
||||
className: "bg-[#1f1f23] text-zinc-400 font-bold border-b border-zinc-800"
|
||||
}, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", {
|
||||
className: "p-2.5 w-1/4"
|
||||
}, "Tên Preset"), /*#__PURE__*/React.createElement("th", {
|
||||
className: "p-2.5 w-1/4"
|
||||
}, "Từ khóa kích hoạt"), /*#__PURE__*/React.createElement("th", {
|
||||
className: "p-2.5 w-1/6"
|
||||
}, "Số Bar"), /*#__PURE__*/React.createElement("th", {
|
||||
className: "p-2.5 w-1/6"
|
||||
}, "BPM"), /*#__PURE__*/React.createElement("th", {
|
||||
className: "p-2.5 w-1/6 text-right"
|
||||
}, "Hành động"))), /*#__PURE__*/React.createElement("tbody", null, filtered.map(p => /*#__PURE__*/React.createElement("tr", {
|
||||
key: p.id,
|
||||
className: "border-b border-zinc-800/50 hover:bg-zinc-850"
|
||||
}, /*#__PURE__*/React.createElement("td", {
|
||||
className: "p-2.5 font-semibold text-purple-300"
|
||||
}, p.name), /*#__PURE__*/React.createElement("td", {
|
||||
className: "p-2.5 text-zinc-400 font-mono text-[11px] truncate max-w-[150px]"
|
||||
}, p.keywords.join(', ')), /*#__PURE__*/React.createElement("td", {
|
||||
className: "p-2.5 text-zinc-400"
|
||||
}, p.default_bars, " Bars"), /*#__PURE__*/React.createElement("td", {
|
||||
className: "p-2.5 text-zinc-400"
|
||||
}, p.default_bpm, " BPM"), /*#__PURE__*/React.createElement("td", {
|
||||
className: "p-2.5 text-right flex items-center justify-end gap-1.5 h-full"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
type: "button",
|
||||
onClick: () => handleEdit(p),
|
||||
className: "px-2 py-0.5 bg-zinc-850 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[10px]"
|
||||
}, "Sửa"), /*#__PURE__*/React.createElement("button", {
|
||||
type: "button",
|
||||
onClick: () => handleDelete(p.id),
|
||||
className: "px-2 py-0.5 bg-red-950/40 hover:bg-red-800 text-red-400 rounded border border-red-900 text-[10px]"
|
||||
}, "Xóa"))))))))) : /*#__PURE__*/React.createElement("form", {
|
||||
onSubmit: handleSaveForm,
|
||||
className: "flex-1 flex flex-col gap-3 min-w-0"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "text-xs font-bold text-zinc-400 shrink-0 border-b border-zinc-800 pb-1"
|
||||
}, editingPreset === 'new' ? "TẠO PRESET MỚI" : `SỬA PRESET: ${editingPreset.name}`), /*#__PURE__*/React.createElement("div", {
|
||||
className: "grid grid-cols-2 gap-3"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "Tên Preset"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
value: formName,
|
||||
onChange: e => setFormName(e.target.value),
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "Danh mục"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
value: formCategory,
|
||||
onChange: e => setFormCategory(e.target.value),
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
|
||||
}))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "Từ khóa kích hoạt (ngăn cách bằng dấu phẩy)"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
value: formKeywords,
|
||||
onChange: e => setFormKeywords(e.target.value),
|
||||
placeholder: "Ví dụ: epic orchestra, hoành tráng, nhạc phim epic",
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "grid grid-cols-3 gap-3"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "Số Bars mặc định"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "number",
|
||||
value: formBars,
|
||||
onChange: e => setFormBars(e.target.value),
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "BPM mặc định"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "number",
|
||||
value: formBpm,
|
||||
onChange: e => setFormBpm(e.target.value),
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "Âm giai (Scale) mặc định"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
value: formScale,
|
||||
onChange: e => setFormScale(e.target.value),
|
||||
className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
|
||||
}))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1 flex-1 min-h-0"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
}, "System Prompt Template / Luật soạn nhạc"), /*#__PURE__*/React.createElement("textarea", {
|
||||
value: formTemplate,
|
||||
onChange: e => setFormTemplate(e.target.value),
|
||||
rows: 6,
|
||||
className: "flex-1 bg-zinc-900 border border-zinc-700 rounded p-2.5 text-xs outline-none focus:border-purple-600 text-zinc-200 font-mono resize-none"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center justify-end gap-2 shrink-0 pt-2 border-t border-zinc-800"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
type: "button",
|
||||
onClick: () => setEditingPreset(null),
|
||||
className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border border-zinc-700 rounded text-xs transition"
|
||||
}, "Quay lại"), /*#__PURE__*/React.createElement("button", {
|
||||
type: "submit",
|
||||
className: "px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold shadow transition"
|
||||
}, "Lưu Preset")))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "p-4 border-t border-zinc-800 flex justify-end shrink-0 bg-[#202024]"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => { setEditingPreset(null); onClose(); },
|
||||
className: "px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs font-bold shadow transition"
|
||||
}, "Đóng"))));
|
||||
};
|
||||
|
||||
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs }) => {
|
||||
const [activeRollTool, setActiveRollTool] = React.useState('select');
|
||||
const [snapVal, setSnapVal] = React.useState('1/16');
|
||||
@@ -5079,6 +5382,7 @@ const App = () => {
|
||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||
const [systemManagerModalOpen, setSystemManagerModalOpen] = useState(false);
|
||||
const [aiConfigModalOpen, setAiConfigModalOpen] = useState(false);
|
||||
const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false);
|
||||
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
|
||||
const [pluginsData, setPluginsData] = useState(null);
|
||||
useEffect(() => {
|
||||
@@ -10206,6 +10510,20 @@ const App = () => {
|
||||
const dawContext = window.AIGateway.buildAIPromptContext({
|
||||
tracks, bpm, selectedTrackId, currentTime, selLeft, selRight
|
||||
});
|
||||
|
||||
let matchedInstruction = '';
|
||||
try {
|
||||
const localPresets = localStorage.getItem('daw_ai_prompt_presets');
|
||||
const presets = localPresets ? JSON.parse(localPresets) : DEFAULT_PRESETS;
|
||||
const matched = presets.find(p => p.keywords.some(kw => prompt.toLowerCase().includes(kw.toLowerCase())));
|
||||
if (matched) {
|
||||
matchedInstruction = matched.system_instruction_template;
|
||||
setAiActionLog(prev => [...prev, { type: 'status', text: ` 🎯 Khớp Preset: "${matched.name}". Tự động tối ưu gợi ý...`, time: Date.now() }]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Lỗi khi tìm preset:", e);
|
||||
}
|
||||
|
||||
const result = await window.AIGateway.executeAIPrompt({
|
||||
prompt: prompt,
|
||||
provider: provider.name || 'default',
|
||||
@@ -10213,7 +10531,8 @@ const App = () => {
|
||||
apiKey: apiKey,
|
||||
baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
|
||||
dawContext,
|
||||
tools: window.AIGateway.DEFAULT_TOOLS
|
||||
tools: window.AIGateway.DEFAULT_TOOLS,
|
||||
systemInstruction: matchedInstruction
|
||||
});
|
||||
const hasText = !!result.textResponse;
|
||||
const hasCalls = result.functionCalls && result.functionCalls.length > 0;
|
||||
@@ -11028,6 +11347,69 @@ const App = () => {
|
||||
pushAction('AI_FADE_OUT', track.id, beforeSnap, afterSnap);
|
||||
}, 50);
|
||||
return { success: true, trackId: track.id, clipId: clip.id, duration_seconds: duration };
|
||||
},
|
||||
generateMultitrackMidi: (args) => {
|
||||
const { composition_title, bpm: aiBpm, total_bars, tracks: aiTracks } = args;
|
||||
if (aiBpm) {
|
||||
setBpm(aiBpm.toString());
|
||||
}
|
||||
const bpmVal = aiBpm || parseInt(bpm) || 120;
|
||||
const secondsPerBeat = 60.0 / bpmVal;
|
||||
const secondsPerBar = secondsPerBeat * 4;
|
||||
const durationSec = total_bars * secondsPerBar;
|
||||
|
||||
updateActiveTracks(prev => {
|
||||
let updatedTracks = [...prev];
|
||||
aiTracks.forEach(aiTrack => {
|
||||
let targetTrack = updatedTracks.find(
|
||||
t => t.name.toLowerCase() === aiTrack.track_name.toLowerCase()
|
||||
);
|
||||
if (!targetTrack) {
|
||||
const newId = (updatedTracks.length + 1).toString();
|
||||
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
|
||||
const selectColor = colors[updatedTracks.length % colors.length];
|
||||
targetTrack = {
|
||||
id: newId,
|
||||
name: aiTrack.track_name,
|
||||
type: 'MIDI',
|
||||
volumeDb: 0,
|
||||
pan: 0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
color: selectColor,
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
clips: [],
|
||||
sections: [],
|
||||
midiItems: [],
|
||||
isArmed: false,
|
||||
monitoringEnabled: true,
|
||||
inputSource: { deviceType: 'NONE', deviceId: '' }
|
||||
};
|
||||
updatedTracks.push(targetTrack);
|
||||
}
|
||||
const itemStartTimeSec = currentTime;
|
||||
const newMidiItem = {
|
||||
id: 'item_ai_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
|
||||
name: `${composition_title || 'AI Theme'} - ${aiTrack.track_name}`,
|
||||
startTime: itemStartTimeSec,
|
||||
duration: durationSec,
|
||||
notes: aiTrack.notes.map((note, index) => ({
|
||||
id: `note_ai_${Date.now()}_${index}`,
|
||||
pitch: note.pitch,
|
||||
start_beat: note.start_beat,
|
||||
duration_beats: note.duration_beats,
|
||||
velocity: note.velocity || 0.8,
|
||||
pan: 0.0
|
||||
}))
|
||||
};
|
||||
targetTrack.midiItems = [...(targetTrack.midiItems || []), newMidiItem];
|
||||
});
|
||||
return updatedTracks;
|
||||
});
|
||||
|
||||
showToast(`Đã nạp ${aiTracks.length} tracks MIDI thế hệ AI!`, 'success');
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
window.DAWCommandDispatcher.registerDAWCommands(api);
|
||||
@@ -11295,6 +11677,10 @@ const App = () => {
|
||||
label: 'Config AI Providers...',
|
||||
icon: 'settings',
|
||||
action: () => setAiConfigModalOpen(true)
|
||||
}, {
|
||||
label: 'AI MIDI Preset Manager...',
|
||||
icon: 'sliders',
|
||||
action: () => setAiPresetModalOpen(true)
|
||||
}, {
|
||||
label: 'DSP Tools Panel',
|
||||
icon: 'wrench',
|
||||
@@ -11990,6 +12376,15 @@ const App = () => {
|
||||
})), " AI Copilot"), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-1"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => setAiPresetModalOpen(true),
|
||||
className: "text-zinc-600 hover:text-zinc-300 mr-0.5",
|
||||
title: "Preset Manager"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "inline-flex items-center shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "sliders",
|
||||
className: "w-3 h-3"
|
||||
}))), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
setAiActionLog([]);
|
||||
showToast('Đã xoá nhật ký AI.', 'info');
|
||||
@@ -13772,6 +14167,9 @@ const App = () => {
|
||||
isOpen: pluginManagerModalOpen,
|
||||
onClose: () => setPluginManagerModalOpen(false),
|
||||
pluginsData: pluginsData
|
||||
}), /*#__PURE__*/React.createElement(AIPresetModal, {
|
||||
isOpen: aiPresetModalOpen,
|
||||
onClose: () => setAiPresetModalOpen(false)
|
||||
}));
|
||||
};
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "sonicforge-studio",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "babel app/static/js/app.jsx --presets=@babel/preset-react -o app/static/js/app.precompiled.js"
|
||||
"build": "babel app/static/js/app.jsx -o app/static/js/app.precompiled.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^8.0.4",
|
||||
|
||||
Reference in New Issue
Block a user