feat(preset-manager): category dropdown + generator modal
This commit is contained in:
+106
-43
@@ -5429,6 +5429,41 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
const [formBpm, setFormBpm] = React.useState(120);
|
||||
const [formScale, setFormScale] = React.useState('C Minor');
|
||||
const [formTemplate, setFormTemplate] = React.useState('');
|
||||
const [showGeneratorModal, setShowGeneratorModal] = React.useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = React.useState('Orchestral / Film Score');
|
||||
const [isAddingCategory, setIsAddingCategory] = React.useState(false);
|
||||
const [newCategoryValue, setNewCategoryValue] = React.useState('');
|
||||
const [categoriesVersion, setCategoriesVersion] = React.useState(0);
|
||||
|
||||
const presetCategories = React.useMemo(() => {
|
||||
const fromPresets = [...new Set(presets.map(p => p.category).filter(Boolean))];
|
||||
let saved = [];
|
||||
try {
|
||||
const raw = localStorage.getItem('midi_prompt_categories');
|
||||
saved = raw ? JSON.parse(raw) : [];
|
||||
} catch (e) { saved = []; }
|
||||
return [...new Set([...fromPresets, ...saved])].sort();
|
||||
}, [presets, categoriesVersion]);
|
||||
|
||||
const addNewCategory = (cat) => {
|
||||
const val = (cat || '').trim();
|
||||
if (!val) return;
|
||||
setFormCategory(val);
|
||||
setSelectedCategory(val);
|
||||
try {
|
||||
const raw = localStorage.getItem('midi_prompt_categories');
|
||||
const list = raw ? JSON.parse(raw) : [];
|
||||
if (!list.includes(val)) {
|
||||
list.push(val);
|
||||
localStorage.setItem('midi_prompt_categories', JSON.stringify(list));
|
||||
setCategoriesVersion(v => v + 1);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const refreshPresets = () => {
|
||||
setPresets([...mgr.getPresets()]);
|
||||
};
|
||||
|
||||
// Sync from backend on mount — merge into local presets, never overwrite
|
||||
React.useEffect(() => {
|
||||
@@ -5471,10 +5506,13 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
setFormName(p.name);
|
||||
setFormKeywords(p.keywords.join(', '));
|
||||
setFormCategory(p.category);
|
||||
setSelectedCategory(p.category);
|
||||
setFormBars(p.default_bars);
|
||||
setFormBpm(p.default_bpm);
|
||||
setFormScale(p.default_scale);
|
||||
setFormTemplate(p.system_instruction_template);
|
||||
setIsAddingCategory(false);
|
||||
setNewCategoryValue('');
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
@@ -5482,50 +5520,18 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
setFormName('');
|
||||
setFormKeywords('');
|
||||
setFormCategory('Orchestral / Film Score');
|
||||
setSelectedCategory('Orchestral / Film Score');
|
||||
setFormBars(8);
|
||||
setFormBpm(120);
|
||||
setFormScale('C Minor');
|
||||
setFormTemplate('');
|
||||
setIsAddingCategory(false);
|
||||
setNewCategoryValue('');
|
||||
};
|
||||
|
||||
const handleNewStructured = () => {
|
||||
setEditingPreset('new');
|
||||
setFormName('');
|
||||
setFormKeywords('');
|
||||
setFormCategory('Orchestral / Film Score');
|
||||
setFormBars(8);
|
||||
setFormBpm(120);
|
||||
setFormScale('C Minor');
|
||||
setFormTemplate(`[Mô tả thể loại / phong cách]
|
||||
Ví dụ: Nhạc phim epic, dàn nhạc giao hưởng, tempo 130 BPM, giọng Cm.
|
||||
|
||||
[Cấu trúc bố cục]
|
||||
- Intro (2 ô nhịp): ...
|
||||
- Phát triển (4 ô nhịp): ...
|
||||
- Climax / Cao trào (2 ô nhịp): ...
|
||||
|
||||
[Yêu cầu về nhạc cụ / Track]
|
||||
1. Track 1 - Strings:
|
||||
- Vai trò: ...
|
||||
- Quãng: ...
|
||||
2. Track 2 - Brass:
|
||||
- Vai trò: ...
|
||||
- Quãng: ...
|
||||
3. Track 3 - Percussion:
|
||||
- Vai trò: ...
|
||||
|
||||
[Yêu cầu hòa âm / Giai điệu]
|
||||
- Tiến trình hợp âm: ...
|
||||
- Pattern giai điệu: ...
|
||||
|
||||
[Hiệu ứng / Sắc thái]
|
||||
- Dynamics: ...
|
||||
- Articulation: ...
|
||||
|
||||
[Lưu ý kỹ thuật MIDI]
|
||||
- Viết note liên tục trên toàn bộ số ô nhịp.
|
||||
- Dùng whole note, half note, quarter note.
|
||||
- Tránh chồng note quá dày.`);
|
||||
refreshPresets();
|
||||
setShowGeneratorModal(true);
|
||||
};
|
||||
|
||||
const handleToggleFav = (id) => {
|
||||
@@ -5598,7 +5604,7 @@ Ví dụ: Nhạc phim epic, dàn nhạc giao hưởng, tempo 130 BPM, giọng Cm
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__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"
|
||||
@@ -5716,12 +5722,50 @@ Ví dụ: Nhạc phim epic, dàn nhạc giao hưởng, tempo 130 BPM, giọng Cm
|
||||
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", {
|
||||
}, "Danh mục"), isAddingCategory ? /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex gap-2"
|
||||
}, /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
value: formCategory,
|
||||
onChange: e => setFormCategory(e.target.value),
|
||||
value: newCategoryValue,
|
||||
onChange: e => setNewCategoryValue(e.target.value),
|
||||
onBlur: () => {
|
||||
if (newCategoryValue.trim()) {
|
||||
addNewCategory(newCategoryValue.trim());
|
||||
}
|
||||
setIsAddingCategory(false);
|
||||
},
|
||||
onKeyDown: e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (newCategoryValue.trim()) {
|
||||
addNewCategory(newCategoryValue.trim());
|
||||
}
|
||||
setIsAddingCategory(false);
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsAddingCategory(false);
|
||||
}
|
||||
},
|
||||
autoFocus: true,
|
||||
className: "flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200",
|
||||
placeholder: "Nhập danh mục mới..."
|
||||
})) : /*#__PURE__*/React.createElement("select", {
|
||||
value: selectedCategory,
|
||||
onChange: e => {
|
||||
if (e.target.value === '__add_new__') {
|
||||
setIsAddingCategory(true);
|
||||
setNewCategoryValue('');
|
||||
} else {
|
||||
setSelectedCategory(e.target.value);
|
||||
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", {
|
||||
}, presetCategories.map(c => /*#__PURE__*/React.createElement("option", {
|
||||
key: c,
|
||||
value: c
|
||||
}, c)), /*#__PURE__*/React.createElement("option", {
|
||||
value: '__add_new__'
|
||||
}, "+ Nhập danh mục mới...")))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col gap-1"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
className: "text-[10px] uppercase font-bold text-zinc-500"
|
||||
@@ -5789,7 +5833,26 @@ Ví dụ: Nhạc phim epic, dàn nhạc giao hưởng, tempo 130 BPM, giọng Cm
|
||||
}, /*#__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"))));
|
||||
}, "Đóng")))), showGeneratorModal ? /*#__PURE__*/React.createElement("div", {
|
||||
className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80 p-4",
|
||||
onClick: e => { if (e.target === e.currentTarget) { refreshPresets(); setShowGeneratorModal(false); } }
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-full h-full max-w-6xl max-h-[90vh] bg-[#13141a] border border-zinc-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center justify-between p-3 border-b border-zinc-800 shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("h3", {
|
||||
className: "text-sm font-bold text-purple-400"
|
||||
}, "AI Prompt Generator"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => { refreshPresets(); setShowGeneratorModal(false); },
|
||||
className: "text-zinc-400 hover:text-white"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "x",
|
||||
className: "w-4 h-4"
|
||||
}))), /*#__PURE__*/React.createElement("iframe", {
|
||||
src: "/ai-prompt-generator",
|
||||
className: "flex-1 w-full border-0 bg-white",
|
||||
title: "AI Prompt Generator"
|
||||
}))) : null);
|
||||
};
|
||||
|
||||
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange }) => {
|
||||
|
||||
Reference in New Issue
Block a user