refactor: remove piano roll AI prompt, reuse Copilot for MIDI gen
- Removed aiPrompt/aiLoading state and handleAIPrompt from PianoRollTabEditor - Removed AI prompt input UI from piano roll toolbar - handleAISend now detects active piano roll tab and routes prompts to MIDI note generation, appends notes to the tab
This commit is contained in:
+47
-61
@@ -5469,8 +5469,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
snapToScaleRef.current = snapToScale;
|
snapToScaleRef.current = snapToScale;
|
||||||
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
|
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
|
||||||
const scaleMenuOriginRef = React.useRef(null);
|
const scaleMenuOriginRef = React.useRef(null);
|
||||||
const [aiPrompt, setAiPrompt] = React.useState('');
|
|
||||||
const [aiLoading, setAiLoading] = React.useState(false);
|
|
||||||
const [showCC, setShowCC] = React.useState(true);
|
const [showCC, setShowCC] = React.useState(true);
|
||||||
const [ccHeight, setCcHeight] = React.useState(80);
|
const [ccHeight, setCcHeight] = React.useState(80);
|
||||||
|
|
||||||
@@ -5485,52 +5483,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
return octave * 12 + best;
|
return octave * 12 + best;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAIPrompt = async () => {
|
|
||||||
const prompt = aiPrompt.trim();
|
|
||||||
if (!prompt || !window.AIGateway) return;
|
|
||||||
setAiLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await window.AIGateway.executeAIPrompt({
|
|
||||||
prompt: 'Generate MIDI notes for a piano roll. Return ONLY a JSON array: [{pitch(0-127), start_beat, duration_beats, velocity(0.0-1.0)}]. ' + prompt,
|
|
||||||
provider: 'openai',
|
|
||||||
model: 'gpt-4o',
|
|
||||||
systemInstruction: 'You are a MIDI composer. Output ONLY valid JSON array of notes. No markdown, no explanation. Use 4/4 time. pitch=C4=60, D4=62, E4=64, F4=65, G4=67, A4=69, B4=71, C5=72.'
|
|
||||||
});
|
|
||||||
let notesData = null;
|
|
||||||
if (result.textResponse) {
|
|
||||||
try {
|
|
||||||
const cleaned = result.textResponse.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
|
|
||||||
notesData = JSON.parse(cleaned);
|
|
||||||
} catch (e1) {}
|
|
||||||
}
|
|
||||||
if (!notesData && result.functionCalls) {
|
|
||||||
for (const fc of result.functionCalls) {
|
|
||||||
if (fc.arguments && fc.arguments.notes) { notesData = fc.arguments.notes; break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Array.isArray(notesData) && notesData.length > 0) {
|
|
||||||
const newNotes = notesData.map((n, i) => ({
|
|
||||||
id: 'note_ai_' + Date.now() + '_' + i,
|
|
||||||
pitch: Math.max(0, Math.min(127, n.pitch || 60)),
|
|
||||||
start_beat: Math.max(0, parseFloat(n.start_beat) || 0),
|
|
||||||
duration_beats: Math.max(0.125, parseFloat(n.duration_beats) || 0.25),
|
|
||||||
velocity: Math.max(0.1, Math.min(1.0, n.velocity ?? 0.8)),
|
|
||||||
pan: 0.0
|
|
||||||
}));
|
|
||||||
pushToUndo(notes);
|
|
||||||
setNotes(prev => [...prev, ...newNotes]);
|
|
||||||
setSelectedNoteIds(newNotes.map(n => n.id));
|
|
||||||
if (window.SonicSF && newNotes.length > 0) {
|
|
||||||
const ctx = getAudioContext();
|
|
||||||
newNotes.forEach(n => window.SonicSF.playNote(n.pitch, n.velocity * 127, 300, ctx.currentTime + (n.start_beat * 0.01), st.instrumentProgram, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('AI Piano Roll error:', err);
|
|
||||||
}
|
|
||||||
setAiLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderScaleContextMenu = () => {
|
const renderScaleContextMenu = () => {
|
||||||
const closeMenu = () => setScaleMenuPos(null);
|
const closeMenu = () => setScaleMenuPos(null);
|
||||||
const origin = scaleMenuOriginRef.current || scaleMenuPos;
|
const origin = scaleMenuOriginRef.current || scaleMenuPos;
|
||||||
@@ -5652,19 +5604,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
onClick: () => setShowCC(!showCC),
|
onClick: () => setShowCC(!showCC),
|
||||||
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
|
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
|
||||||
}, ccMode === 'pan' ? 'Pan' : 'Vel'), /*#__PURE__*/React.createElement("div", {
|
}, ccMode === 'pan' ? 'Pan' : 'Vel'), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex items-center gap-1 flex-1 max-w-[300px] ml-2"
|
|
||||||
}, /*#__PURE__*/React.createElement("input", {
|
|
||||||
type: "text",
|
|
||||||
value: aiPrompt,
|
|
||||||
onChange: e => setAiPrompt(e.target.value),
|
|
||||||
onKeyDown: e => { if (e.key === 'Enter') handleAIPrompt(); },
|
|
||||||
placeholder: "AI: tạo 8 bars MIDI...",
|
|
||||||
className: "flex-1 bg-zinc-900 border border-zinc-700 text-zinc-200 text-[10px] rounded px-2 py-1 outline-none focus:border-amber-500 min-w-0"
|
|
||||||
}), /*#__PURE__*/React.createElement("button", {
|
|
||||||
onClick: handleAIPrompt,
|
|
||||||
disabled: aiLoading,
|
|
||||||
className: "px-2 py-1 text-[10px] bg-purple-700 hover:bg-purple-600 disabled:bg-zinc-700 text-white rounded flex items-center gap-1 transition"
|
|
||||||
}, aiLoading ? "..." : "AI")), /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "flex items-center gap-1"
|
className: "flex items-center gap-1"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
||||||
@@ -12296,6 +12235,53 @@ const App = () => {
|
|||||||
const handleAISend = async () => {
|
const handleAISend = async () => {
|
||||||
const prompt = aiPrompt.trim();
|
const prompt = aiPrompt.trim();
|
||||||
if (!prompt) { showToast('Vui lòng nhập nội dung prompt.', 'warning'); return; }
|
if (!prompt) { showToast('Vui lòng nhập nội dung prompt.', 'warning'); return; }
|
||||||
|
|
||||||
|
// Check if piano roll tab is active → route prompt to MIDI generation
|
||||||
|
const activePianoRoll = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
|
||||||
|
if (activePianoRoll) {
|
||||||
|
setAiProcessing(true);
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'status', text: ` ⏳ Đang gửi prompt đến AI cho Piano Roll...`, time: Date.now() }]);
|
||||||
|
try {
|
||||||
|
const result = await window.AIGateway.executeAIPrompt({
|
||||||
|
prompt: 'Generate MIDI notes for a piano roll. Return ONLY a JSON array: [{pitch(0-127), start_beat, duration_beats, velocity(0.0-1.0)}]. ' + prompt,
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4o',
|
||||||
|
systemInstruction: 'You are a MIDI composer. Output ONLY valid JSON array of notes. No markdown, no explanation. Use 4/4 time. pitch=C4=60, D4=62, E4=64, F4=65, G4=67, A4=69, B4=71, C5=72.'
|
||||||
|
});
|
||||||
|
let notesData = null;
|
||||||
|
if (result.textResponse) {
|
||||||
|
try {
|
||||||
|
const cleaned = result.textResponse.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
|
||||||
|
notesData = JSON.parse(cleaned);
|
||||||
|
} catch (e1) {}
|
||||||
|
}
|
||||||
|
if (!notesData && result.functionCalls) {
|
||||||
|
for (const fc of result.functionCalls) {
|
||||||
|
if (fc.arguments && fc.arguments.notes) { notesData = fc.arguments.notes; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(notesData) && notesData.length > 0) {
|
||||||
|
const newNotes = notesData.map((n, i) => ({
|
||||||
|
id: 'note_ai_' + Date.now() + '_' + i,
|
||||||
|
pitch: Math.max(0, Math.min(127, n.pitch || 60)),
|
||||||
|
start_beat: Math.max(0, parseFloat(n.start_beat) || 0),
|
||||||
|
duration_beats: Math.max(0.125, parseFloat(n.duration_beats) || 0.25),
|
||||||
|
velocity: Math.max(0.1, Math.min(1.0, n.velocity ?? 0.8)),
|
||||||
|
pan: 0.0
|
||||||
|
}));
|
||||||
|
setSubTabs(prev => prev.map(s => s.id === activePianoRoll.id ? { ...s, notes: [...(s.notes || []), ...newNotes], isDirty: true } : s));
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ Đã thêm ${newNotes.length} notes vào Piano Roll`, time: Date.now() }]);
|
||||||
|
setCanvasRedrawCount(n => n + 1);
|
||||||
|
} else {
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ AI không trả về notes hợp lệ.`, time: Date.now() }]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ Lỗi: ${err.message}`, time: Date.now() }]);
|
||||||
|
}
|
||||||
|
setAiProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (window.DAWCommandDispatcher) {
|
if (window.DAWCommandDispatcher) {
|
||||||
window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId;
|
window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId;
|
||||||
window.DAWCommandDispatcher.currentTracks = activeTracks;
|
window.DAWCommandDispatcher.currentTracks = activeTracks;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user