fix: sửa copilot không gửi AI provider 2
This commit is contained in:
+67
-48
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user