feat(preset-manager): category dropdown + generator modal

This commit is contained in:
2026-07-31 05:50:55 +07:00
parent 5e8664af1c
commit bc951e75af
4 changed files with 128 additions and 79 deletions
+106 -43
View File
@@ -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]
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 }) => {
File diff suppressed because one or more lines are too long