fix: sửa copilot không gửi AI provider 2

This commit is contained in:
2026-07-22 17:01:41 +07:00
parent 8a85dd2dfc
commit d6fe1326c5
5 changed files with 205 additions and 329 deletions
+67 -48
View File
@@ -3031,6 +3031,7 @@ const App = () => {
const [numberBar, setNumberBar] = useState(1);
const [subTabHeight, setSubTabHeight] = useState(96);
const [isExporting, setIsExporting] = useState(false);
const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
const [soloedTrackId, setSoloedTrackId] = useState(null);
const [toastMessage, setToastMessage] = useState(null);
const [showAIConfig, setShowAIConfig] = useState(false);
@@ -3048,7 +3049,7 @@ const App = () => {
ai: 'right',
python_tools: 'bottom',
selection: 'bottom',
media_explorer: 'right',
media_explorer: 'bottom',
fx_rack: 'bottom',
midi_events: 'bottom'
});
@@ -3952,6 +3953,11 @@ const App = () => {
return;
}
}
if (ctrl && !alt && e.key === 's') {
e.preventDefault();
handleSaveProject();
return;
}
if (!ctrl && !alt && e.key === 's') {
e.preventDefault();
handleSplitTrack(selectedTrackId);
@@ -6338,6 +6344,19 @@ const App = () => {
clientSideExport(exportTracks);
}
};
const handleSaveProject = async () => {
if (!currentUser) { setIsMandatoryLogin(false); setAuthMode('login'); setAuthModalOpen(true); return; }
let name = projectName;
if (!name) { name = prompt("Nhập tên dự án:", "Dự án SonicForge"); if (!name) return; }
setProjectName(name);
localStorage.setItem('sonic_project_name', name);
const ss = arr => (arr || []).map(t => ({ id: t.id, name: t.name, startTime: t.startTime, height: t.height, volumeDb: t.volumeDb, pan: t.pan, muted: t.muted, solo: t.solo, color: t.color, markers: t.markers || [], serverFileId: t.serverFileId || null, channelInfo: t.channelInfo ? { channels: t.channelInfo.channels, isStereo: t.channelInfo.isStereo, label: t.channelInfo.label } : null }));
try {
const dataJson = JSON.stringify({ id: 'project_' + Date.now(), name, tracks: ss(tracks) });
await window.SonicAPI.saveCloudProject(name, dataJson);
showToast(`Đã lưu "${name}" lên server!`, "success");
} catch (err) { showToast(err.message || "Lỗi lưu server", "error"); }
};
const handleSaveCloud = async () => {
if (!currentUser) {
setIsMandatoryLogin(false);
@@ -7788,18 +7807,14 @@ const App = () => {
action: () => handleImportSFS()
}, {
label: 'Save Project',
icon: 'save',
icon: 'upload-cloud',
shortcut: 'Ctrl+S',
action: () => handleExportSFS()
action: () => handleSaveProject()
}, {
label: 'Save As...',
icon: 'save',
icon: 'download',
shortcut: 'Ctrl+Alt+S',
action: () => handleExportSFS()
}, {
label: 'Save to Cloud',
icon: 'upload-cloud',
action: () => handleSaveCloud()
}, {
sep: true
}, {
@@ -8479,7 +8494,7 @@ const App = () => {
addPanel('ai', panelPositions.ai, showAIPanel);
addPanel('python_tools', panelPositions.python_tools || 'bottom', showPythonToolsPanel);
addPanel('selection', panelPositions.selection, showSelectionPanel);
addPanel('media_explorer', panelPositions.media_explorer || 'right', showMediaExplorer);
addPanel('media_explorer', 'bottom', showMediaExplorer);
addPanel('fx_rack', panelPositions.fx_rack || 'bottom', showFxRack);
addPanel('midi_events', panelPositions.midi_events || 'bottom', showMidiEvents);
const closePanel = id => {
@@ -8575,7 +8590,7 @@ const App = () => {
className: "w-3 h-3"
})), isExporting ? '...' : 'Export'));
if (panelId === 'ai') return /*#__PURE__*/React.createElement("div", {
className: "flex flex-col h-full gap-1.5"
className: "flex flex-col gap-1.5 flex-1 min-h-0"
}, /*#__PURE__*/React.createElement("div", {
className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
onMouseDown: e => startPanelDrag('ai', e)
@@ -8630,6 +8645,43 @@ const App = () => {
key: p.id,
value: p.id
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => {
if (window.DAWCommandDispatcher && window.DAWCommandDispatcher.undo) {
const entry = window.DAWCommandDispatcher.undo();
if (entry) {
setAiActionLog(prev => [...prev, { type: 'undo', text: `Undo: ${entry.name}`, time: Date.now() }]);
showToast(`Undo AI: ${entry.name}`, 'info');
}
} else {
handleUndo();
setAiActionLog(prev => [...prev, { type: 'undo', text: 'Undo (Ctrl+Z)', time: Date.now() }]);
}
},
className: "w-full text-[10px] text-amber-400 hover:text-amber-300 border border-amber-800 rounded py-0.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "rotate-ccw",
className: "w-3 h-3"
}), "Undo")), /*#__PURE__*/React.createElement("div", {
className: "flex-1 min-h-0 flex flex-col overflow-hidden mt-1"
}, /*#__PURE__*/React.createElement("div", {
className: "text-[10px] font-bold text-zinc-400 uppercase shrink-0 pb-0.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "list",
className: "w-3 h-3"
}), " Action Log")), /*#__PURE__*/React.createElement("div", {
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 select-text"
}, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 italic select-text"
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
key: i,
className: `text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ${entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300'}`
}, new Date(entry.time).toLocaleTimeString(), entry.text)))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
@@ -8666,8 +8718,8 @@ const App = () => {
}
}
}
}), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1"
})), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("button", {
onClick: handleAISend,
disabled: aiProcessing,
@@ -8684,38 +8736,8 @@ const App = () => {
},
className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded text-xs border border-zinc-700"
}, "Clear")), /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 mt-0.5"
}, "Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1 mt-1 flex-1 min-h-0 flex flex-col overflow-hidden"
}, /*#__PURE__*/React.createElement("div", {
className: "text-[10px] font-bold text-zinc-400 uppercase flex items-center justify-between shrink-0 pb-0.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "list",
className: "w-3 h-3"
}), " Action Log"), aiActionLog.length > 0 && /*#__PURE__*/React.createElement("button", {
onClick: () => {
if (window.DAWCommandDispatcher && window.DAWCommandDispatcher.undo) {
const entry = window.DAWCommandDispatcher.undo();
if (entry) {
setAiActionLog(prev => [...prev, { type: 'undo', text: `Undo: ${entry.name}`, time: Date.now() }]);
showToast(`Undo AI: ${entry.name}`, 'info');
}
} else {
handleUndo();
setAiActionLog(prev => [...prev, { type: 'undo', text: 'Undo (Ctrl+Z)', time: Date.now() }]);
}
},
className: "text-[10px] text-amber-400 hover:text-amber-300 border border-amber-800 rounded px-1 py-0"
}, "Undo"))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 select-text"
}, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 italic select-text"
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
key: i,
className: `text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ${entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300'}`
}, new Date(entry.time).toLocaleTimeString(), entry.text))));
className: "text-xs text-zinc-600 shrink-0"
}, "Enter để gửi nhanh")));
if (panelId === 'python_tools') return /*#__PURE__*/React.createElement("div", {
className: "flex flex-col h-full gap-1.5"
}, /*#__PURE__*/React.createElement("div", {
@@ -8950,10 +8972,7 @@ const App = () => {
}, panels.map((p, idx) => /*#__PURE__*/React.createElement(React.Fragment, {
key: p
}, /*#__PURE__*/React.createElement("div", {
className: `${p === 'media_explorer' ? 'flex-shrink-0' : 'flex-1 min-h-0'} border border-zinc-700 rounded-lg bg-[#262626] shadow-sm`,
style: p === 'media_explorer' ? {
height: `${mediaExplorerHeight}%`
} : {}
className: 'flex flex-col flex-1 min-h-0 border border-zinc-700 rounded-lg bg-[#262626] shadow-sm'
}, renderPanelContent(p)), idx < panels.length - 1 && /*#__PURE__*/React.createElement("div", {
className: "h-1.5 cursor-ns-resize hover:bg-cyan-500/50 transition-colors rounded shrink-0",
onMouseDown: startRowResize
+106 -56
View File
@@ -3074,6 +3074,7 @@ const App = () => {
const [numberBar, setNumberBar] = useState(1);
const [subTabHeight, setSubTabHeight] = useState(96);
const [isExporting, setIsExporting] = useState(false);
const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
const [soloedTrackId, setSoloedTrackId] = useState(null);
const [toastMessage, setToastMessage] = useState(null);
const [showAIConfig, setShowAIConfig] = useState(false);
@@ -3091,7 +3092,7 @@ const App = () => {
ai: 'right',
python_tools: 'bottom',
selection: 'bottom',
media_explorer: 'right',
media_explorer: 'bottom',
fx_rack: 'bottom',
midi_events: 'bottom'
});
@@ -4001,6 +4002,11 @@ const App = () => {
return;
}
}
if (ctrl && !alt && e.key === 's') {
e.preventDefault();
handleSaveProject();
return;
}
if (!ctrl && !alt && e.key === 's') {
e.preventDefault();
handleSplitTrack(selectedTrackId);
@@ -6442,6 +6448,50 @@ const App = () => {
clientSideExport(exportTracks);
}
};
const handleSaveProject = async () => {
if (!currentUser) {
setIsMandatoryLogin(false);
setAuthMode('login');
setAuthModalOpen(true);
return;
}
let name = projectName;
if (!name) {
name = prompt("Nhập tên dự án:", "Dự án SonicForge");
if (!name) return;
}
setProjectName(name);
localStorage.setItem('sonic_project_name', name);
const ss = arr => (arr || []).map(t => ({
id: t.id,
name: t.name,
startTime: t.startTime,
height: t.height,
volumeDb: t.volumeDb,
pan: t.pan,
muted: t.muted,
solo: t.solo,
color: t.color,
markers: t.markers || [],
serverFileId: t.serverFileId || null,
channelInfo: t.channelInfo ? {
channels: t.channelInfo.channels,
isStereo: t.channelInfo.isStereo,
label: t.channelInfo.label
} : null
}));
try {
const dataJson = JSON.stringify({
id: 'project_' + Date.now(),
name,
tracks: ss(tracks)
});
await window.SonicAPI.saveCloudProject(name, dataJson);
showToast(`Đã lưu "${name}" lên server!`, "success");
} catch (err) {
showToast(err.message || "Lỗi lưu server", "error");
}
};
const handleSaveCloud = async () => {
if (!currentUser) {
setIsMandatoryLogin(false);
@@ -8135,18 +8185,14 @@ const App = () => {
action: () => handleImportSFS()
}, {
label: 'Save Project',
icon: 'save',
icon: 'upload-cloud',
shortcut: 'Ctrl+S',
action: () => handleExportSFS()
action: () => handleSaveProject()
}, {
label: 'Save As...',
icon: 'save',
icon: 'download',
shortcut: 'Ctrl+Alt+S',
action: () => handleExportSFS()
}, {
label: 'Save to Cloud',
icon: 'upload-cloud',
action: () => handleSaveCloud()
}, {
sep: true
}, {
@@ -8826,7 +8872,7 @@ const App = () => {
addPanel('ai', panelPositions.ai, showAIPanel);
addPanel('python_tools', panelPositions.python_tools || 'bottom', showPythonToolsPanel);
addPanel('selection', panelPositions.selection, showSelectionPanel);
addPanel('media_explorer', panelPositions.media_explorer || 'right', showMediaExplorer);
addPanel('media_explorer', 'bottom', showMediaExplorer);
addPanel('fx_rack', panelPositions.fx_rack || 'bottom', showFxRack);
addPanel('midi_events', panelPositions.midi_events || 'bottom', showMidiEvents);
const closePanel = id => {
@@ -8969,7 +9015,7 @@ const App = () => {
className: "w-3 h-3"
})), isExporting ? '...' : 'Export'));
if (panelId === 'ai') return /*#__PURE__*/React.createElement("div", {
className: "flex flex-col h-full gap-1.5"
className: "flex flex-col gap-1.5 flex-1 min-h-0"
}, /*#__PURE__*/React.createElement("div", {
className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
onMouseDown: e => startPanelDrag('ai', e)
@@ -9024,6 +9070,51 @@ const App = () => {
key: p.id,
value: p.id
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => {
if (window.DAWCommandDispatcher && window.DAWCommandDispatcher.undo) {
const entry = window.DAWCommandDispatcher.undo();
if (entry) {
setAiActionLog(prev => [...prev, {
type: 'undo',
text: `Undo: ${entry.name}`,
time: Date.now()
}]);
showToast(`Undo AI: ${entry.name}`, 'info');
}
} else {
handleUndo();
setAiActionLog(prev => [...prev, {
type: 'undo',
text: 'Undo (Ctrl+Z)',
time: Date.now()
}]);
}
},
className: "w-full text-[10px] text-amber-400 hover:text-amber-300 border border-amber-800 rounded py-0.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "rotate-ccw",
className: "w-3 h-3"
}), "Undo")), /*#__PURE__*/React.createElement("div", {
className: "flex-1 min-h-0 flex flex-col overflow-hidden mt-1"
}, /*#__PURE__*/React.createElement("div", {
className: "text-[10px] font-bold text-zinc-400 uppercase shrink-0 pb-0.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "list",
className: "w-3 h-3"
}), " Action Log")), /*#__PURE__*/React.createElement("div", {
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 select-text"
}, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 italic select-text"
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
key: i,
className: `text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ${entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300'}`
}, new Date(entry.time).toLocaleTimeString(), entry.text)))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
@@ -9060,8 +9151,8 @@ const App = () => {
}
}
}
}), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1"
})), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("button", {
onClick: handleAISend,
disabled: aiProcessing,
@@ -9078,46 +9169,8 @@ const App = () => {
},
className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded text-xs border border-zinc-700"
}, "Clear")), /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 mt-0.5"
}, "Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1 mt-1 flex-1 min-h-0 flex flex-col overflow-hidden"
}, /*#__PURE__*/React.createElement("div", {
className: "text-[10px] font-bold text-zinc-400 uppercase flex items-center justify-between shrink-0 pb-0.5"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "list",
className: "w-3 h-3"
}), " Action Log"), aiActionLog.length > 0 && /*#__PURE__*/React.createElement("button", {
onClick: () => {
if (window.DAWCommandDispatcher && window.DAWCommandDispatcher.undo) {
const entry = window.DAWCommandDispatcher.undo();
if (entry) {
setAiActionLog(prev => [...prev, {
type: 'undo',
text: `Undo: ${entry.name}`,
time: Date.now()
}]);
showToast(`Undo AI: ${entry.name}`, 'info');
}
} else {
handleUndo();
setAiActionLog(prev => [...prev, {
type: 'undo',
text: 'Undo (Ctrl+Z)',
time: Date.now()
}]);
}
},
className: "text-[10px] text-amber-400 hover:text-amber-300 border border-amber-800 rounded px-1 py-0"
}, "Undo"))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 select-text"
}, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 italic select-text"
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
key: i,
className: `text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ${entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300'}`
}, new Date(entry.time).toLocaleTimeString(), entry.text))));
className: "text-xs text-zinc-600 shrink-0"
}, "Enter để gửi nhanh")));
if (panelId === 'python_tools') return /*#__PURE__*/React.createElement("div", {
className: "flex flex-col h-full gap-1.5"
}, /*#__PURE__*/React.createElement("div", {
@@ -9352,10 +9405,7 @@ const App = () => {
}, panels.map((p, idx) => /*#__PURE__*/React.createElement(React.Fragment, {
key: p
}, /*#__PURE__*/React.createElement("div", {
className: `${p === 'media_explorer' ? 'flex-shrink-0' : 'flex-1 min-h-0'} border border-zinc-700 rounded-lg bg-[#262626] shadow-sm`,
style: p === 'media_explorer' ? {
height: `${mediaExplorerHeight}%`
} : {}
className: 'flex flex-col flex-1 min-h-0 border border-zinc-700 rounded-lg bg-[#262626] shadow-sm'
}, renderPanelContent(p)), idx < panels.length - 1 && /*#__PURE__*/React.createElement("div", {
className: "h-1.5 cursor-ns-resize hover:bg-cyan-500/50 transition-colors rounded shrink-0",
onMouseDown: startRowResize
+30 -225
View File
@@ -3,249 +3,49 @@
const AIGateway = (function() {
const DEFAULT_TOOLS = [{
name: 'create_track',
description: 'Tạo một track âm thanh mới trong dự án',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'Tên cho track mới (VD: Beat, Vocal, Guitar)' },
type: { type: 'string', enum: ['audio', 'midi'], description: 'Loại track' }
},
required: ['name']
}
name: 'set_selection', description: 'Chọn vùng timeline', parameters: { type: 'object', properties: { start_bar: { type: 'number' }, end_bar: { type: 'number' }, start_time: { type: 'number' }, end_time: { type: 'number' }, length_bars: { type: 'number' } } }
}, {
name: 'delete_track',
description: 'Xóa một track khỏi dự án',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track cần xóa. Nếu không có thì xóa track đang chọn.' }
}
}
name: 'cut_audio', description: 'Cắt audio, snap zero-crossing, tạo track mới', parameters: { type: 'object', properties: { track_id: { type: 'string' }, start_time: { type: 'number' }, end_time: { type: 'number' }, start_bar: { type: 'number' }, end_bar: { type: 'number' }, length_bars: { type: 'number' }, snap_silence: { type: 'boolean' }, new_track_name: { type: 'string' } } }
}, {
name: 'add_clip',
description: 'Thêm một clip âm thanh rỗng vào track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track đích' },
start_time: { type: 'number', description: 'Vị trí bắt đầu (giây)' },
duration_seconds: { type: 'number', description: 'Độ dài clip (giây)' },
start_bar: { type: 'number', description: 'Vị trí bắt đầu (bar). Bar 0 = bar đầu tiên. Dùng thay cho start_time.' },
length_bars: { type: 'number', description: 'Độ dài (bar). Dùng thay cho duration_seconds.' },
name: { type: 'string', description: 'Tên clip' }
}
}
name: 'create_track', description: 'Tạo track mới', parameters: { type: 'object', properties: { name: { type: 'string' }, type: { type: 'string', enum: ['audio', 'midi'] } }, required: ['name'] }
}, {
name: 'remove_clip',
description: 'Xóa một clip khỏi track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track' },
clip_id: { type: 'string', description: 'ID của clip cần xóa' }
},
required: ['clip_id']
}
name: 'delete_track', description: 'Xóa track', parameters: { type: 'object', properties: { track_id: { type: 'string' } } }
}, {
name: 'set_track_volume',
description: 'Điều chỉnh âm lượng của track (dB)',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track' },
volume_db: { type: 'number', description: 'Âm lượng tính bằng dB (VD: -6, 0, +3)' }
},
required: ['volume_db']
}
name: 'rename_track', description: 'Đổi tên track', parameters: { type: 'object', properties: { track_id: { type: 'string' }, name: { type: 'string' } }, required: ['track_id', 'name'] }
}, {
name: 'set_track_pan',
description: 'Điều chỉnh cân bằng trái/phải (pan) của track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track' },
pan: { type: 'integer', description: 'Pan value: -100 (trái), 0 (trung tâm), 100 (phải)' }
},
required: ['pan']
}
name: 'add_clip', description: 'Thêm clip rỗng vào track', parameters: { type: 'object', properties: { track_id: { type: 'string' }, start_time: { type: 'number' }, duration_seconds: { type: 'number' }, start_bar: { type: 'number' }, length_bars: { type: 'number' }, name: { type: 'string' } } }
}, {
name: 'toggle_mute',
description: 'Bật/tắt mute (tắt tiếng) của track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track cần mute/unmute' }
}
}
name: 'remove_clip', description: 'Xóa clip khỏi track', parameters: { type: 'object', properties: { track_id: { type: 'string' }, clip_id: { type: 'string' } }, required: ['clip_id'] }
}, {
name: 'toggle_solo',
description: 'Bật/tắt solo (chỉ nghe track này) của track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track cần solo/unsolo' }
}
}
name: 'set_track_volume', description: 'Chỉnh âm lượng dB', parameters: { type: 'object', properties: { track_id: { type: 'string' }, volume_db: { type: 'number' } }, required: ['volume_db'] }
}, {
name: 'rename_track',
description: 'Đổi tên của một track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track cần đổi tên' },
name: { type: 'string', description: 'Tên mới cho track' }
},
required: ['track_id', 'name']
}
name: 'set_track_pan', description: 'Chỉnh pan trái/phải', parameters: { type: 'object', properties: { track_id: { type: 'string' }, pan: { type: 'integer' } }, required: ['pan'] }
}, {
name: 'set_bpm',
description: 'Thay đổi tempo (BPM) của dự án',
parameters: {
type: 'object',
properties: {
bpm: { type: 'number', description: 'Tempo mới tính bằng BPM (VD: 120, 128, 140)' }
},
required: ['bpm']
}
name: 'toggle_mute', description: 'Mute/unmute track', parameters: { type: 'object', properties: { track_id: { type: 'string' } } }
}, {
name: 'set_playhead',
description: 'Di chuyển playhead (con trỏ phát) đến vị trí chỉ định',
parameters: {
type: 'object',
properties: {
time: { type: 'number', description: 'Vị trí thời gian tính bằng giây' },
bar: { type: 'number', description: 'Vị trí bar (0 = bar đầu tiên). Dùng thay cho time.' }
}
}
name: 'toggle_solo', description: 'Solo/unsolo track', parameters: { type: 'object', properties: { track_id: { type: 'string' } } }
}, {
name: 'add_marker',
description: 'Thêm một marker (đánh dấu) vào track tại vị trí chỉ định',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track' },
time: { type: 'number', description: 'Vị trí thời gian (giây). Mặc định là vị trí playhead hiện tại.' },
label: { type: 'string', description: 'Nhãn cho marker' }
}
}
name: 'set_bpm', description: 'Thay đổi BPM', parameters: { type: 'object', properties: { bpm: { type: 'number' } }, required: ['bpm'] }
}, {
name: 'process_audio_dsp',
description: 'Xử lý hiệu ứng âm thanh DSP cho track (chuẩn hóa, đảo phase, gain, pitch shift)',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track âm thanh' },
action: { type: 'string', enum: ['normalize', 'invert_phase', 'gain', 'pitch_shift'], description: 'Loại xử lý DSP' },
params: { type: 'object', description: 'Tham số bổ sung: gain_db cho gain, semitones cho pitch_shift' }
},
required: ['track_id', 'action']
}
name: 'set_playhead', description: 'Di chuyển playhead', parameters: { type: 'object', properties: { time: { type: 'number' }, bar: { type: 'number' } } }
}, {
name: 'create_midi_item',
description: 'Thêm một MIDI item/clip vào track',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track đích' },
start_bar: { type: 'number', description: 'Vị trí bắt đầu (tính bằng bar)' },
length_bars: { type: 'number', description: 'Độ dài item (tính bằng bar)' }
},
required: ['track_id', 'start_bar', 'length_bars']
}
name: 'add_marker', description: 'Thêm marker', parameters: { type: 'object', properties: { track_id: { type: 'string' }, time: { type: 'number' }, label: { type: 'string' } } }
}, {
name: 'set_selection',
description: 'Chọn một vùng trên timeline (selection range). Dùng để xác định khoảng thời gian trước khi gọi các lệnh khác.',
parameters: {
type: 'object',
properties: {
start_bar: { type: 'number', description: 'Bar bắt đầu (0 = bar đầu tiên của project)' },
end_bar: { type: 'number', description: 'Bar kết thúc' },
start_time: { type: 'number', description: 'Thời gian bắt đầu (giây). Dùng thay cho start_bar.' },
end_time: { type: 'number', description: 'Thời gian kết thúc (giây). Dùng thay cho end_bar.' },
length_bars: { type: 'number', description: 'Độ dài vùng chọn (bar). Dùng cùng start_bar thay cho end_bar.' }
}
}
name: 'process_audio_dsp', description: 'Xử lý DSP: normalize/invert/gain/pitch', parameters: { type: 'object', properties: { track_id: { type: 'string' }, action: { type: 'string', enum: ['normalize', 'invert_phase', 'gain', 'pitch_shift'] }, params: { type: 'object' } }, required: ['track_id', 'action'] }
}, {
name: 'select_item',
description: 'Chọn một item/clip/sample cụ thể trên track. Nếu không có track_id và item_name thì chọn toàn bộ timeline.',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track. Nếu không có thì dùng track đang chọn.' },
item_name: { type: 'string', description: 'Tên của clip/sample cần chọn. VD: "Cut_Cartoon Capers Loop.mp3". Nếu không có thì chọn toàn bộ track.' },
select_all: { type: 'boolean', description: 'Chọn toàn bộ timeline (bỏ qua track_id và item_name)' }
}
}
name: 'create_midi_item', description: 'Tạo MIDI item trên track', parameters: { type: 'object', properties: { track_id: { type: 'string' }, start_bar: { type: 'number' }, length_bars: { type: 'number' } }, required: ['track_id', 'start_bar', 'length_bars'] }
}, {
name: 'cut_audio',
description: 'Cắt một đoạn audio từ track nguồn. Tự động tạo selection, snap zero-crossing, và tạo track mới chứa đoạn cắt. Lệnh DUY NHẤT cho thao tác cắt - không cần gọi set_selection trước.',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track nguồn' },
start_time: { type: 'number', description: 'Vị trí bắt đầu cắt (giây). Mặc định: dùng playhead hiện tại.' },
end_time: { type: 'number', description: 'Vị trí kết thúc cắt (giây). Mặc định: dùng selection hiện tại.' },
start_bar: { type: 'number', description: 'Bar bắt đầu cắt (0 = bar đầu). Dùng thay cho start_time.' },
end_bar: { type: 'number', description: 'Bar kết thúc cắt. VD: end_bar=12 cắt đến bar 12. Dùng thay cho end_time.' },
length_bars: { type: 'number', description: 'Độ dài cắt (bar). Dùng cùng start_bar thay cho end_bar.' },
snap_silence: { type: 'boolean', description: 'Tự động snap vào điểm zero-crossing gần nhất ở hai đầu (mặc định: true)' },
new_track_name: { type: 'string', description: 'Tên cho track mới. Mặc định: "Cut_<tên_track_gốc>"' }
}
}
name: 'modify_midi_notes', description: 'Sửa note MIDI trong item', parameters: { type: 'object', properties: { item_id: { type: 'string' }, notes: { type: 'array', items: { type: 'object', properties: { pitch: { type: 'string' }, start_time: { type: 'number' }, duration: { type: 'number' }, velocity: { type: 'integer', minimum: 0, maximum: 127 } }, required: ['pitch', 'start_time', 'duration'] } } }, required: ['item_id', 'notes'] }
}, {
name: 'scan_track',
description: 'Quét và phân tích track âm thanh: phát hiện BPM (tempo) và tự động cập nhật tempo hệ thống, sample rate, số kênh (mono/stereo), duration',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track cần quét' },
set_tempo: { type: 'boolean', description: 'Tự động cập nhật tempo hệ thống (mặc định: true)' }
}
}
name: 'select_item', description: 'Chọn clip/item theo tên', parameters: { type: 'object', properties: { track_id: { type: 'string' }, item_name: { type: 'string' }, select_all: { type: 'boolean' } } }
}, {
name: 'modify_midi_notes',
description: 'Thêm, chỉnh sửa hoặc xóa các note MIDI trong item',
parameters: {
type: 'object',
properties: {
item_id: { type: 'string', description: 'ID của MIDI item' },
notes: {
type: 'array',
description: 'Danh sách các note MIDI',
items: {
type: 'object',
properties: {
pitch: { type: 'string', description: 'VD: C4, D#3, F5' },
start_time: { type: 'number', description: 'Thời điểm bắt đầu (bar hoặc giây)' },
duration: { type: 'number', description: 'Độ dài note' },
velocity: { type: 'integer', minimum: 0, maximum: 127, description: 'Độ mạnh 0-127' }
},
required: ['pitch', 'start_time', 'duration']
}
}
},
required: ['item_id', 'notes']
}
name: 'scan_track', description: 'Phân tích track: BPM, SR, kênh', parameters: { type: 'object', properties: { track_id: { type: 'string' }, set_tempo: { type: 'boolean' } } }
}, {
name: 'export_audio',
description: 'Xuất (export/render) âm thanh ra file WAV/MP3/OGG và tải về. Lệnh DUY NHẤT cho thao tác xuất file - không cần gọi lệnh khác.',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track cần xuất. Nếu không có thì dùng track đang chọn.' },
format: { type: 'string', enum: ['wav', 'mp3', 'ogg'], description: 'Định dạng file xuất' },
sample_rate: { type: 'string', enum: ['22500', '44100'], description: 'Sample rate (Hz). Chỉ dùng cho WAV.' },
bit_depth: { type: 'string', enum: ['8', '16', '24'], description: 'Bit depth. Chỉ dùng cho WAV.' },
quality: { type: 'string', enum: ['44khz', 'lossless'], description: 'Chất lượng. Dùng cho MP3/OGG.' },
channels: { type: 'string', enum: ['mono', 'stereo'], description: 'Số kênh (mono/stereo)' },
start_time: { type: 'number', description: 'Vị trí bắt đầu xuất (giây).' },
end_time: { type: 'number', description: 'Vị trí kết thúc xuất (giây).' },
start_bar: { type: 'number', description: 'Bar bắt đầu (0 = bar đầu). Dùng thay cho start_time.' },
length_bars: { type: 'number', description: 'Độ dài (bar). Dùng cùng start_bar thay cho end_time.' }
},
required: ['format']
}
name: 'fade_in', description: 'Fade-in clip (0.5s đến max)', parameters: { type: 'object', properties: { track_id: { type: 'string' }, duration_seconds: { type: 'number' } } }
}, {
name: 'export_audio', description: 'Xuất file WAV/MP3/OGG và tải về', parameters: { type: 'object', properties: { track_id: { type: 'string' }, format: { type: 'string', enum: ['wav', 'mp3', 'ogg'] }, sample_rate: { type: 'string', enum: ['22500', '44100'] }, bit_depth: { type: 'string', enum: ['8', '16', '24'] }, quality: { type: 'string', enum: ['44khz', 'lossless'] }, channels: { type: 'string', enum: ['mono', 'stereo'] }, start_time: { type: 'number' }, end_time: { type: 'number' }, start_bar: { type: 'number' }, length_bars: { type: 'number' } }, required: ['format'] }
}, {
name: 'fade_out', description: 'Fade-out clip (0.5s đến max)', parameters: { type: 'object', properties: { track_id: { type: 'string' }, duration_seconds: { type: 'number' } } }
}];
function parseOrigin(urlStr) {
@@ -343,7 +143,7 @@ const AIGateway = (function() {
const contextStr = JSON.stringify(context, null, 2);
const toolNames = DEFAULT_TOOLS.map(t => ` - ${t.name}: ${t.description}`).join('\n');
return [
{ role: 'system', content: `Bạn là trợ lý AI cho DAW. Dùng function calls để thực hiện yêu cầu. Các function có sẵn:\n${toolNames}\n\nHƯỚNG DẪN:\n- Để xuất file: gọi export_audio với format, sample_rate, bit_depth, channels, start_time, end_time.\n- Để chọn vùng: gọi set_selection với start_time/end_time hoặc start_bar/end_bar.\n- Bar 0 = bar đầu tiên.\n- Có thể gọi NHIỀU function cùng lúc, không cần chờ kết quả function trước.` },
{ role: 'system', content: `Bạn là trợ lý DAW. Dùng function calls. Bar 0 = bar đầu. Có thể gọi nhiều function cùng lúc.` },
{ role: 'user', content: `Ngữ cảnh DAW hiện tại:\n${contextStr}\n\nYêu cầu người dùng: ${prompt}` }
];
}
@@ -391,6 +191,11 @@ const AIGateway = (function() {
toolChoice: 'auto'
});
if (completion && completion.error) {
const errMsg = completion.error.message || completion.error.code || JSON.stringify(completion.error);
throw new Error(`AI Provider error: ${errMsg}`);
}
const functionCalls = extractFunctionCalls(completion);
const textResponse = completion.choices && completion.choices[0] && completion.choices[0].message && completion.choices[0].message.content
? completion.choices[0].message.content
@@ -59,6 +59,8 @@ const DAWCommandDispatcher = (function() {
register('PROCESS_AUDIO_DSP', (args) => api.processAudioDsp(args));
register('RENAME_TRACK', (args) => api.renameTrack(args));
register('SCAN_TRACK', (args) => api.scanTrack(args));
register('FADE_IN', (args) => api.fadeIn(args));
register('FADE_OUT', (args) => api.fadeOut(args));
register('CUT_AUDIO', (args) => api.cutAudio(args));
register('SET_SELECTION', (args) => api.setSelection(args));
register('EXPORT_AUDIO', (args) => api.exportAudio(args));
Binary file not shown.