From 6fd9db54cfa5a9b18f3dcedb79c8c42c8fe677fa Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 09:59:43 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20s=E1=BB=ADa=20c=C3=A1c=20tools=20c?= =?UTF-8?q?=E1=BB=A7a=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 78 ++++++++++-- app/static/js/app.precompiled.js | 112 ++++++++++++++++-- app/static/js/services/aiGateway.js | 11 ++ .../js/services/dawCommandDispatcher.js | 1 + app/storage/sonicforge.db | Bin 45056 -> 45056 bytes 5 files changed, 188 insertions(+), 14 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 2ad3703..5bec07b 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -3190,6 +3190,7 @@ const App = () => { try { const profile = await window.SonicAPI.getProfile(); setCurrentUser(profile); + localStorage.setItem('sonic_user', JSON.stringify(profile)); if (profile.must_change_password) { setIsMandatoryLogin(true); setAuthMode('force_change'); @@ -3199,12 +3200,19 @@ const App = () => { setAuthModalOpen(false); } } catch (err) { - localStorage.removeItem('sonic_token'); - localStorage.removeItem('sonic_user'); - setCurrentUser(null); - setIsMandatoryLogin(true); - setAuthMode('login'); - setAuthModalOpen(true); + const cached = localStorage.getItem('sonic_user'); + if (cached) { + try { setCurrentUser(JSON.parse(cached)); } catch (_) {} + setIsMandatoryLogin(false); + setAuthModalOpen(false); + } else { + localStorage.removeItem('sonic_token'); + localStorage.removeItem('sonic_user'); + setCurrentUser(null); + setIsMandatoryLogin(true); + setAuthMode('login'); + setAuthModalOpen(true); + } } }; checkAuthStatus(); @@ -7470,6 +7478,45 @@ const App = () => { handlePlayheadSet(time); return { success: true, time: parseFloat(time.toFixed(3)) }; }, + selectItem: (args) => { + if (args.select_all) { + setSelectedTrackId(null); + clearLocalSelection(); + setSelectionMode('global'); + setSelectionStart(0); + const maxDur = tracks.reduce((max, t) => { + const dur = t.buffer ? t.buffer.duration : 0; + const clips = t.clips || []; + const clipMax = clips.reduce((m, c) => Math.max(m, (c.startTime || 0) + (c.buffer ? c.buffer.duration : 0)), 0); + return Math.max(max, dur, clipMax); + }, 0); + setSelectionEnd(Math.max(maxDur, currentTime + 10)); + return { success: true, selection: 'all', duration: parseFloat(Math.max(maxDur, currentTime + 10).toFixed(3)) }; + } + const tid = args.track_id || selectedTrackId; + const track = tracks.find(t => t.id === tid); + if (!track) return { success: false, error: `Track ${tid} not found` }; + const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, name: track.name, buffer: track.buffer, startTime: track.startTime || 0 }] : []); + if (args.item_name) { + const match = clips.find(c => c.name && c.name.toLowerCase().includes(args.item_name.toLowerCase())); + if (!match) return { success: false, error: `No clip matching "${args.item_name}" on track ${track.name}` }; + setSelectedTrackId(tid); + clearLocalSelection(); + setSelectionMode('global'); + const start = match.startTime || 0; + const end = start + (match.buffer ? match.buffer.duration : 2); + setSelectionStart(start); + setSelectionEnd(end); + return { success: true, trackId: tid, trackName: track.name, clipId: match.id, clipName: match.name, start: parseFloat(start.toFixed(3)), end: parseFloat(end.toFixed(3)) }; + } + setSelectedTrackId(tid); + clearLocalSelection(); + setSelectionMode('global'); + const trackEnd = track.buffer ? track.buffer.duration : (clips.length > 0 ? Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration : 0))) : 4); + setSelectionStart(0); + setSelectionEnd(trackEnd); + return { success: true, trackId: tid, trackName: track.name, duration: parseFloat(trackEnd.toFixed(3)) }; + }, addMarker: (args) => { const trackId = args.track_id || selectedTrackId; const time = args.time ?? currentTime; @@ -7741,7 +7788,7 @@ const App = () => { onClick: () => setMenuOpen(menuOpen === menu.label ? null : menu.label), className: `px-3 py-1 text-xs font-medium transition rounded ${menuOpen === menu.label ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}` }, menu.label), menuOpen === menu.label && /*#__PURE__*/React.createElement("div", { - className: "absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-52 z-50", + className: "absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 z-50", onClick: () => setMenuOpen(null) }, menu.items.map((item, i) => item.sep ? /*#__PURE__*/React.createElement("div", { key: i, @@ -8892,6 +8939,23 @@ const App = () => { setEditNameInput(track.name); } }, track.name)), /*#__PURE__*/React.createElement("div", { + className: "flex flex-wrap gap-0.5 max-w-[100px] mb-0.5" + }, (track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ id: 'default', name: track.name, startTime: track.startTime }] : []).slice(0, 3).map(c => /*#__PURE__*/React.createElement("span", { + key: c.id, + className: "text-[9px] font-mono text-zinc-500 bg-zinc-800/60 rounded px-0.5 truncate max-w-[90px] cursor-pointer hover:text-cyan-400 hover:bg-zinc-700", + title: c.name || track.name, + onClick: e => { + e.stopPropagation(); + setSelectedTrackId(track.id); + clearLocalSelection(); + setSelectionMode('global'); + const start = c.startTime || 0; + const end = start + (c.buffer ? c.buffer.duration : 2); + setSelectionStart(start); + setSelectionEnd(end); + showToast(`Selected: ${c.name || track.name}`, 'info'); + } + }, c.name || track.name))), /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-1" }, /*#__PURE__*/React.createElement("button", { onClick: e => { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 68d55f8..c6b98ab 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -3190,6 +3190,7 @@ const App = () => { try { const profile = await window.SonicAPI.getProfile(); setCurrentUser(profile); + localStorage.setItem('sonic_user', JSON.stringify(profile)); if (profile.must_change_password) { setIsMandatoryLogin(true); setAuthMode('force_change'); @@ -3199,12 +3200,21 @@ const App = () => { setAuthModalOpen(false); } } catch (err) { - localStorage.removeItem('sonic_token'); - localStorage.removeItem('sonic_user'); - setCurrentUser(null); - setIsMandatoryLogin(true); - setAuthMode('login'); - setAuthModalOpen(true); + const cached = localStorage.getItem('sonic_user'); + if (cached) { + try { + setCurrentUser(JSON.parse(cached)); + } catch (_) {} + setIsMandatoryLogin(false); + setAuthModalOpen(false); + } else { + localStorage.removeItem('sonic_token'); + localStorage.removeItem('sonic_user'); + setCurrentUser(null); + setIsMandatoryLogin(true); + setAuthMode('login'); + setAuthModalOpen(true); + } } }; checkAuthStatus(); @@ -7635,6 +7645,73 @@ const App = () => { time: parseFloat(time.toFixed(3)) }; }, + selectItem: args => { + if (args.select_all) { + setSelectedTrackId(null); + clearLocalSelection(); + setSelectionMode('global'); + setSelectionStart(0); + const maxDur = tracks.reduce((max, t) => { + const dur = t.buffer ? t.buffer.duration : 0; + const clips = t.clips || []; + const clipMax = clips.reduce((m, c) => Math.max(m, (c.startTime || 0) + (c.buffer ? c.buffer.duration : 0)), 0); + return Math.max(max, dur, clipMax); + }, 0); + setSelectionEnd(Math.max(maxDur, currentTime + 10)); + return { + success: true, + selection: 'all', + duration: parseFloat(Math.max(maxDur, currentTime + 10).toFixed(3)) + }; + } + const tid = args.track_id || selectedTrackId; + const track = tracks.find(t => t.id === tid); + if (!track) return { + success: false, + error: `Track ${tid} not found` + }; + const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ + id: 'default_' + track.id, + name: track.name, + buffer: track.buffer, + startTime: track.startTime || 0 + }] : []; + if (args.item_name) { + const match = clips.find(c => c.name && c.name.toLowerCase().includes(args.item_name.toLowerCase())); + if (!match) return { + success: false, + error: `No clip matching "${args.item_name}" on track ${track.name}` + }; + setSelectedTrackId(tid); + clearLocalSelection(); + setSelectionMode('global'); + const start = match.startTime || 0; + const end = start + (match.buffer ? match.buffer.duration : 2); + setSelectionStart(start); + setSelectionEnd(end); + return { + success: true, + trackId: tid, + trackName: track.name, + clipId: match.id, + clipName: match.name, + start: parseFloat(start.toFixed(3)), + end: parseFloat(end.toFixed(3)) + }; + } + setSelectedTrackId(tid); + clearLocalSelection(); + setSelectionMode('global'); + const trackEnd = track.buffer ? track.buffer.duration : clips.length > 0 ? Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration : 0))) : 4; + setSelectionStart(0); + setSelectionEnd(trackEnd); + return { + success: true, + trackId: tid, + trackName: track.name, + duration: parseFloat(trackEnd.toFixed(3)) + }; + }, addMarker: args => { const trackId = args.track_id || selectedTrackId; const time = args.time ?? currentTime; @@ -7928,7 +8005,7 @@ const App = () => { onClick: () => setMenuOpen(menuOpen === menu.label ? null : menu.label), className: `px-3 py-1 text-xs font-medium transition rounded ${menuOpen === menu.label ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}` }, menu.label), menuOpen === menu.label && /*#__PURE__*/React.createElement("div", { - className: "absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-52 z-50", + className: "absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 z-50", onClick: () => setMenuOpen(null) }, menu.items.map((item, i) => item.sep ? /*#__PURE__*/React.createElement("div", { key: i, @@ -9081,6 +9158,27 @@ const App = () => { setEditNameInput(track.name); } }, track.name)), /*#__PURE__*/React.createElement("div", { + className: "flex flex-wrap gap-0.5 max-w-[100px] mb-0.5" + }, (track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ + id: 'default', + name: track.name, + startTime: track.startTime + }] : []).slice(0, 3).map(c => /*#__PURE__*/React.createElement("span", { + key: c.id, + className: "text-[9px] font-mono text-zinc-500 bg-zinc-800/60 rounded px-0.5 truncate max-w-[90px] cursor-pointer hover:text-cyan-400 hover:bg-zinc-700", + title: c.name || track.name, + onClick: e => { + e.stopPropagation(); + setSelectedTrackId(track.id); + clearLocalSelection(); + setSelectionMode('global'); + const start = c.startTime || 0; + const end = start + (c.buffer ? c.buffer.duration : 2); + setSelectionStart(start); + setSelectionEnd(end); + showToast(`Selected: ${c.name || track.name}`, 'info'); + } + }, c.name || track.name))), /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-1" }, /*#__PURE__*/React.createElement("button", { onClick: e => { diff --git a/app/static/js/services/aiGateway.js b/app/static/js/services/aiGateway.js index 043bf1d..3ea9321 100644 --- a/app/static/js/services/aiGateway.js +++ b/app/static/js/services/aiGateway.js @@ -166,6 +166,17 @@ const AIGateway = (function() { length_bars: { type: 'number', description: 'Độ dài vùng chọn (bar). Dùng cùng start_bar thay cho end_bar.' } } } + }, { + 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: 'cut_audio', description: 'Cắt đoạn audio đang được chọn từ track nguồn, snap zero-crossing, tạo track mới chứa đoạn cắt', diff --git a/app/static/js/services/dawCommandDispatcher.js b/app/static/js/services/dawCommandDispatcher.js index e32671f..72a9e37 100644 --- a/app/static/js/services/dawCommandDispatcher.js +++ b/app/static/js/services/dawCommandDispatcher.js @@ -63,6 +63,7 @@ const DAWCommandDispatcher = (function() { register('SET_SELECTION', (args) => api.setSelection(args)); register('SET_BPM', (args) => api.setBpm(args)); register('SET_PLAYHEAD', (args) => api.setPlayhead(args)); + register('SELECT_ITEM', (args) => api.selectItem(args)); register('ADD_MARKER', (args) => api.addMarker(args)); register('CREATE_MIDI_ITEM', (args) => AIGateway.createMidiItem(args)); register('MODIFY_MIDI_NOTES', (args) => AIGateway.modifyMidiNotes(args)); diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index bf8521e6743cfc6b4e078afd986803be3616a0bc..0fa4385e051884de63ff343221bc174b896a4ed7 100644 GIT binary patch delta 136 zcmZp8z|`=7X@WFk^+XwG#_Ej;OY_;ePcrbY=RUbvP+%`NXL~XeXMCuAZ{p^LnpKSK zmWGCA#>OU-4eN3y&#o0TD)YtLNup&T_mpgQw}_$)e5c>jMn{wNWu* delta 103 zcmV-t0GR)P-~xc)0+1U4bdelG0d%oorEdoecK{Es40p2;5Niw(hBX2SUsXSg1qlKN zbY*RDUz7fOB(wf|egOwKGBY$cG&hrjeQX1Ca&?nVe@K%%ey)?2e-8=*yg}NS3##41 JB(tx7Q7{f!C1wBs