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:
2026-07-25 16:19:44 +07:00
parent 2fed3eee0b
commit 5628802e08
2 changed files with 50 additions and 63 deletions
+47 -61
View File
@@ -5469,8 +5469,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
snapToScaleRef.current = snapToScale;
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
const scaleMenuOriginRef = React.useRef(null);
const [aiPrompt, setAiPrompt] = React.useState('');
const [aiLoading, setAiLoading] = React.useState(false);
const [showCC, setShowCC] = React.useState(true);
const [ccHeight, setCcHeight] = React.useState(80);
@@ -5485,52 +5483,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
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 closeMenu = () => setScaleMenuPos(null);
const origin = scaleMenuOriginRef.current || scaleMenuPos;
@@ -5652,19 +5604,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
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'}`
}, 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"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
@@ -12296,6 +12235,53 @@ const App = () => {
const handleAISend = async () => {
const prompt = aiPrompt.trim();
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) {
window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId;
window.DAWCommandDispatcher.currentTracks = activeTracks;