fix: MIDI Rearrange — selected item triggers dedicated tool flow

- midiExtractor.js: extractSelectedMIDIContext with note name conversion
- aiGateway.js: REARRANGE_TOOL_SPEC + buildRearrangeMessage
- dawCommandDispatcher.js: register REARRANGE_MIDI_MELODY command
- app.jsx: handleAISend detects selected MIDI item → rearrange flow
- rearrangeMidiMelody handler places A/B track below source track
This commit is contained in:
2026-07-28 10:54:35 +07:00
parent 421535ca0e
commit 2eb327056d
6 changed files with 285 additions and 0 deletions
+157
View File
@@ -13718,6 +13718,92 @@ const App = () => {
return;
}
// MIDI Rearrange Flow: selected MIDI item use rearrange tool
if (selectedItemIds && selectedItemIds.size === 1) {
const selId = selectedItemIds.values().next().value;
let isMidiItem = false;
let sourceTrackId = null;
let sourceItemName = null;
for (const t of activeTracks || []) {
const found = (t.midiItems || []).find(m => m.id === selId);
if (found) { isMidiItem = true; sourceTrackId = t.id; sourceItemName = found.name; break; }
}
if (isMidiItem && window.SonicMidiExtractor) {
try {
setAiProcessing(true);
setAiActionLog(prev => [...prev, { type: 'status', text: ` 🎯 Phát hiện MIDI item được chọn — chuyển sang chế độ Rearrange...`, time: Date.now() }]);
if (aiProviders.length === 0 || !selectedProviderId) {
try { const d = await window.SonicAPI.getAIConfigs(); if (d && d.providers && d.providers.length > 0) { setAiProviders(d.providers); const a = d.providers.find(p => p.is_active) || d.providers[0]; if (a) setSelectedProviderId(a.id); } } catch (e) {}
}
const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
const provider = prv || aiConfig;
const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
const apiKey = provider.api_key || provider.apiKey || '';
const model = provider.model_name || provider.model || 'deepseek-chat';
setAiActionLog(prev => [...prev, { type: 'info', text: ` Rearrange Provider: ${provider.name || 'default'} | Model: ${model}`, time: Date.now() }]);
const srcContext = window.SonicMidiExtractor.extractSelectedMIDIContext(activeTracks || tracks, selId, bpm);
setAiActionLog(prev => [...prev, { type: 'info', text: ` 📋 Trích xuất ${srcContext.total_notes} notes từ "${srcContext.item_name}" (${srcContext.track_name})`, time: Date.now() }]);
if (window.DAWCommandDispatcher) {
window.DAWCommandDispatcher.rearrangeSourceTrackId = sourceTrackId;
window.DAWCommandDispatcher.rearrangeSourceItemName = sourceItemName;
}
const messages = window.AIGateway.buildRearrangeMessage(prompt, srcContext);
const completion = await window.AIGateway.callLLM({
provider: provider.name || 'default',
model,
apiKey,
baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
messages,
tools: [window.AIGateway.REARRANGE_TOOL_SPEC],
toolChoice: 'auto'
});
const functionCalls = window.AIGateway.extractFunctionCalls(completion);
if (functionCalls && functionCalls.length > 0) {
for (const fc of functionCalls) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult;
setAiActionLog(prev => [...prev, { type: 'status', text: `${fc.name}: thành công — ${fc.arguments.rearrange_title || ''}`, time: Date.now() }]);
} catch (cmdErr) {
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
}
}
}
} else {
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ AI không trả về lệnh rearrange hợp lệ.`, time: Date.now() }]);
}
const textResp = functionCalls.length === 0 && completion.choices && completion.choices[0] && completion.choices[0].message && completion.choices[0].message.content;
if (textResp) {
setAiActionLog(prev => [...prev, { type: 'status', text: ` AI: ${textResp.slice(0, 500)}`, time: Date.now() }]);
}
if (prompt) {
promptHistRef.current = [...promptHistRef.current.slice(-49), prompt];
setPromptHistory(promptHistRef.current);
}
setPromptHistIdx(-1);
setAiActionLog(prev => [...prev, { type: 'status', text: ` Hoàn tất Rearrange.`, time: Date.now() }]);
setAiPrompt('');
setTimeout(() => lucide.createIcons(), 200);
setAiProcessing(false);
return;
} catch (err) {
setAiActionLog(prev => [...prev, { type: 'error', text: ` Lỗi Rearrange: ${err.message}`, time: Date.now() }]);
showToast(`AI Rearrange Error: ${err.message}`, 'error');
setAiProcessing(false);
return;
}
}
}
if (window.DAWCommandDispatcher) {
window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId;
window.DAWCommandDispatcher.currentTracks = activeTracks;
@@ -14670,6 +14756,77 @@ const App = () => {
showToast(`Đã nạp ${aiTracks.length} tracks MIDI thế hệ AI!`, 'success');
return { success: true };
},
rearrangeMidiMelody: (args) => {
const { rearrange_title, rearranged_notes, soundfont_id, soundfont_bank, soundfont_program } = args;
if (!rearranged_notes || rearranged_notes.length === 0) {
return { success: false, error: 'No rearranged notes provided' };
}
const sourceTrackId = window.DAWCommandDispatcher?.rearrangeSourceTrackId || selectedTrackId;
const sourceItemName = window.DAWCommandDispatcher?.rearrangeSourceItemName || 'Source';
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
const secondsPerBar = secondsPerBeat * 4;
const totalBeats = rearranged_notes.reduce((max, n) => Math.max(max, (n.start_beat || 0) + (n.duration_beats || 1)), 0);
const totalBars = Math.max(1, Math.ceil(totalBeats / 4));
const durationSec = totalBars * secondsPerBar;
updateActiveTracks(prev => {
let updatedTracks = [...prev];
// Find source track index to insert new track right after it
const sourceIdx = sourceTrackId ? updatedTracks.findIndex(t => t.id === sourceTrackId || t.id === 'track_' + sourceTrackId) : -1;
const insertIdx = sourceIdx >= 0 ? sourceIdx + 1 : updatedTracks.length;
const newId = (updatedTracks.length + 1).toString();
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
const newTrack = {
id: newId,
name: `[AI Rearrange] ${rearrange_title || 'Variation'}`,
type: 'MIDI',
volumeDb: 0,
pan: 0,
muted: false,
solo: false,
color: colors[insertIdx % colors.length],
markers: [],
serverFileId: null,
clips: [],
sections: [],
midiItems: [{
id: 'item_rearr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
name: `[AI] ${rearrange_title || 'Rearranged'} - var`,
parent_track_id: newId,
startTime: currentTime,
duration: durationSec,
length_bars: totalBars,
notes: rearranged_notes.map((n, i) => ({
id: `note_rearr_${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
}))
}]
};
if (soundfont_id || (soundfont_bank !== undefined && soundfont_program !== undefined)) {
newTrack.soundfont_id = soundfont_id || '';
newTrack.soundfont_bank = soundfont_bank ?? 0;
newTrack.soundfont_program = soundfont_program ?? 0;
newTrack.synth_engine = {
type: 'soundfont',
plugin_id: soundfont_id ? 'sf_' + soundfont_id : null,
soundfont_bank: soundfont_bank ?? 0,
soundfont_program: soundfont_program ?? 0,
soundfont_id: soundfont_id || ''
};
}
updatedTracks.splice(insertIdx, 0, newTrack);
return updatedTracks;
});
showToast(`✅ AI Rearrange: "${rearrange_title}" — ${rearranged_notes.length} notes`, 'success');
return { success: true, trackId: newId, totalBars };
},
createMidiItem: (args) => {
const trackId = args.track_id || selectedTrackId;
if (!trackId) return { success: false, error: 'No track_id provided' };