fix: sửa UX các tools của AI

This commit is contained in:
2026-07-22 10:17:56 +07:00
parent 6fd9db54cf
commit 58089f40f9
3 changed files with 240 additions and 110 deletions
+100 -58
View File
@@ -3074,6 +3074,7 @@ const App = () => {
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
const [editNameInput, setEditNameInput] = useState('');
// Context Menu & Clipboard
@@ -3248,24 +3249,26 @@ const App = () => {
setIsMandatoryLogin(false);
setAuthModalOpen(false);
loadPendingSfsProject();
const loadPrefs = (p) => {
if (!p) return;
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
if (p.panelPositions) setPanelPositions(p.panelPositions);
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
};
(async () => {
try {
const data = await window.SonicAPI.getPreferences();
if (data && data.preferences) {
const p = data.preferences;
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
if (p.panelPositions) setPanelPositions(p.panelPositions);
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
}
} catch (e) { /* preferences not available */ }
if (data && data.preferences) loadPrefs(data.preferences);
else { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
} catch (e) { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
})();
}
};
@@ -4638,10 +4641,10 @@ const App = () => {
const ctx = getAudioContext();
const targetTrack = tracks.find(t => t.id === targetTrackId);
const newClip = {
id: 'clip_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
id: nextClipId(),
startTime: pasteTime,
buffer: clipBuffer,
name: name || 'Pasted Clip'
name: (name || 'Pasted Clip').replace(/\.\w+$/, '') + ' (Pasted)'
};
if (targetTrack) {
setTracks(p => p.map(t => {
@@ -5218,6 +5221,16 @@ const App = () => {
}
}
if (updatedTime >= maxDurationRef.current) {
if (isLoopingSelection) {
stopAllPlayback();
startOffsetTimeRef.current = 0;
startAudioTimeRef.current = context.currentTime;
startTrackPlayback(0);
setCurrentTime(0);
setIsPlaying(true);
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
return;
}
stopAllPlayback();
setCurrentTime(0);
return;
@@ -5559,6 +5572,9 @@ const App = () => {
hoveredTrackIdRef.current = hoveredTrackId;
const captureTrackSnapshotRef = useRef(null);
captureTrackSnapshotRef.current = captureTrackSnapshot;
let clipSeqCounter = 0;
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
const track = tracks.find(t => t.id === trackId);
if (!track) return;
@@ -5571,48 +5587,27 @@ const App = () => {
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
if (!clip) return;
const beforeSnap = captureTrackSnapshot(trackId);
let targetClipId = clip.id;
if (isDuplicate) {
const cloneId = clip.id + '_copy_' + Date.now();
const clone = {
...clip,
id: cloneId,
name: clip.name + ' (Copy)'
};
setDraggedClip({
trackId, clipId: clip.id, clickOffset,
buffer: clip.buffer, name: clip.name, beforeSnap,
isDuplicate: true,
origTrackId: trackId, origClipId: clip.id
});
return;
}
if (!track.clips || track.clips.length === 0) {
setTracks(prev => prev.map(t => {
if (t.id === trackId) {
const newClips = [...existingClips, clone];
return {
...t,
clips: newClips,
buffer: newClips[0].buffer,
startTime: newClips[0].startTime,
name: newClips[0].name
};
return { ...t, clips: existingClips };
}
return t;
}));
targetClipId = cloneId;
} else {
if (!track.clips || track.clips.length === 0) {
setTracks(prev => prev.map(t => {
if (t.id === trackId) {
return {
...t,
clips: existingClips
};
}
return t;
}));
}
}
setDraggedClip({
trackId: trackId,
clipId: targetClipId,
clickOffset: clickOffset,
buffer: clip.buffer,
name: clip.name,
beforeSnap: beforeSnap
trackId, clipId: clip.id, clickOffset,
buffer: clip.buffer, name: clip.name, beforeSnap,
isDuplicate: false
});
};
const stretchedClipRef = useRef(null);
@@ -5769,10 +5764,22 @@ const App = () => {
const handleMouseUp = () => {
const drag = draggedClipRef.current;
if (!drag) return;
if (drag.isDuplicate) {
const cloneId = nextClipId();
const clone = { id: cloneId, buffer: drag.buffer, startTime: 0, name: drag.name.replace(/\.\w+$/, '') + ' (Copy)' };
setTracks(prev => prev.map(t => {
if (t.id === drag.trackId) {
const clips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
return { ...t, clips: [...clips, clone], buffer: clips.length > 0 ? clips[0].buffer : drag.buffer };
}
return t;
}));
setDraggedClip({ ...drag, clipId: cloneId, isDuplicate: false });
}
const afterSnap = captureTrackSnapshotRef.current(drag.trackId);
pushAction('MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
pushAction(drag.isDuplicate ? 'DUPLICATE_CLIP' : 'MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
setDraggedClip(null);
showToast('Đã di chuyển clip.', 'success');
showToast(drag.isDuplicate ? 'Đã sao chép clip.' : 'Đã di chuyển clip.', 'success');
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
@@ -6074,6 +6081,13 @@ const App = () => {
return next;
});
};
const updateClipName = (trackId, clipId, newName) => {
setTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
const clips = t.clips && t.clips.length > 0 ? t.clips : [];
return { ...t, clips: clips.map(c => c.id === clipId ? { ...c, name: newName } : c) };
}));
};
// Load File on Track (with server upload)
const loadFileOnTrack = async (trackId, file) => {
@@ -7238,7 +7252,7 @@ const App = () => {
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = 0;
const clipId = 'clip_' + Date.now();
const clipId = nextClipId();
setTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
const clips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
@@ -7547,9 +7561,11 @@ const App = () => {
panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId
};
useEffect(() => {
if (!currentUser) return;
const prefs = prefsRef.current;
localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
if (!currentUser || currentUser === 'cached') return;
const timer = setTimeout(async () => {
try { await window.SonicAPI.savePreferences(prefsRef.current); } catch (e) {}
try { await window.SonicAPI.savePreferences(prefs); } catch (e) {}
}, 2000);
return () => clearTimeout(timer);
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
@@ -8165,7 +8181,7 @@ const App = () => {
}), /*#__PURE__*/React.createElement("button", {
onClick: () => setIsLoopingSelection(prev => !prev),
className: `w-7 h-7 flex items-center justify-center rounded border transition ${isLoopingSelection ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
title: "Bật/Tắt Lặp vùng chọn"
title: isLoopingSelection ? (selLeft !== null && selRight !== null ? "Loop vùng chọn" : "Loop timeline") : "Bật loop"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
@@ -8955,7 +8971,33 @@ const App = () => {
setSelectionEnd(end);
showToast(`Selected: ${c.name || track.name}`, 'info');
}
}, c.name || track.name))), /*#__PURE__*/React.createElement("div", {
}, c.name || track.name), editingClipName && editingClipName.trackId === track.id && editingClipName.clipId === c.id ? /*#__PURE__*/React.createElement("input", {
type: "text",
value: editNameInput,
autoFocus: true,
onChange: e => setEditNameInput(e.target.value),
onBlur: () => {
if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim());
setEditingClipName(null);
},
onKeyDown: e => {
if (e.key === 'Enter') { if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim()); setEditingClipName(null); }
if (e.key === 'Escape') setEditingClipName(null);
},
onClick: e => e.stopPropagation(),
className: "w-20 text-[9px] font-mono bg-black text-cyan-300 border border-cyan-500 rounded px-0.5 py-0 outline-none"
}) : /*#__PURE__*/React.createElement("button", {
className: "text-[9px] text-zinc-600 hover:text-cyan-400 ml-0.5 shrink-0",
title: "Sửa tên clip",
onClick: e => {
e.stopPropagation();
setEditingClipName({ trackId: track.id, clipId: c.id });
setEditNameInput(c.name || track.name);
}
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "pencil",
className: "w-2.5 h-2.5"
})))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: e => {
+140 -52
View File
@@ -3074,6 +3074,7 @@ const App = () => {
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
const [editNameInput, setEditNameInput] = useState('');
// ── Context Menu & Clipboard ──
@@ -3250,24 +3251,31 @@ const App = () => {
setIsMandatoryLogin(false);
setAuthModalOpen(false);
loadPendingSfsProject();
const loadPrefs = p => {
if (!p) return;
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
if (p.panelPositions) setPanelPositions(p.panelPositions);
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
};
(async () => {
try {
const data = await window.SonicAPI.getPreferences();
if (data && data.preferences) {
const p = data.preferences;
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
if (p.panelPositions) setPanelPositions(p.panelPositions);
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
if (data && data.preferences) loadPrefs(data.preferences);else {
const cached = localStorage.getItem('sonic_preferences');
if (cached) loadPrefs(JSON.parse(cached));
}
} catch (e) {/* preferences not available */}
} catch (e) {
const cached = localStorage.getItem('sonic_preferences');
if (cached) loadPrefs(JSON.parse(cached));
}
})();
}
};
@@ -4639,10 +4647,10 @@ const App = () => {
const ctx = getAudioContext();
const targetTrack = tracks.find(t => t.id === targetTrackId);
const newClip = {
id: 'clip_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
id: nextClipId(),
startTime: pasteTime,
buffer: clipBuffer,
name: name || 'Pasted Clip'
name: (name || 'Pasted Clip').replace(/\.\w+$/, '') + ' (Pasted)'
};
if (targetTrack) {
setTracks(p => p.map(t => {
@@ -5219,6 +5227,16 @@ const App = () => {
}
}
if (updatedTime >= maxDurationRef.current) {
if (isLoopingSelection) {
stopAllPlayback();
startOffsetTimeRef.current = 0;
startAudioTimeRef.current = context.currentTime;
startTrackPlayback(0);
setCurrentTime(0);
setIsPlaying(true);
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
return;
}
stopAllPlayback();
setCurrentTime(0);
return;
@@ -5560,6 +5578,8 @@ const App = () => {
hoveredTrackIdRef.current = hoveredTrackId;
const captureTrackSnapshotRef = useRef(null);
captureTrackSnapshotRef.current = captureTrackSnapshot;
let clipSeqCounter = 0;
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
const track = tracks.find(t => t.id === trackId);
if (!track) return;
@@ -5572,48 +5592,39 @@ const App = () => {
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
if (!clip) return;
const beforeSnap = captureTrackSnapshot(trackId);
let targetClipId = clip.id;
if (isDuplicate) {
const cloneId = clip.id + '_copy_' + Date.now();
const clone = {
...clip,
id: cloneId,
name: clip.name + ' (Copy)'
};
setDraggedClip({
trackId,
clipId: clip.id,
clickOffset,
buffer: clip.buffer,
name: clip.name,
beforeSnap,
isDuplicate: true,
origTrackId: trackId,
origClipId: clip.id
});
return;
}
if (!track.clips || track.clips.length === 0) {
setTracks(prev => prev.map(t => {
if (t.id === trackId) {
const newClips = [...existingClips, clone];
return {
...t,
clips: newClips,
buffer: newClips[0].buffer,
startTime: newClips[0].startTime,
name: newClips[0].name
clips: existingClips
};
}
return t;
}));
targetClipId = cloneId;
} else {
if (!track.clips || track.clips.length === 0) {
setTracks(prev => prev.map(t => {
if (t.id === trackId) {
return {
...t,
clips: existingClips
};
}
return t;
}));
}
}
setDraggedClip({
trackId: trackId,
clipId: targetClipId,
clickOffset: clickOffset,
trackId,
clipId: clip.id,
clickOffset,
buffer: clip.buffer,
name: clip.name,
beforeSnap: beforeSnap
beforeSnap,
isDuplicate: false
});
};
const stretchedClipRef = useRef(null);
@@ -5770,10 +5781,40 @@ const App = () => {
const handleMouseUp = () => {
const drag = draggedClipRef.current;
if (!drag) return;
if (drag.isDuplicate) {
const cloneId = nextClipId();
const clone = {
id: cloneId,
buffer: drag.buffer,
startTime: 0,
name: drag.name.replace(/\.\w+$/, '') + ' (Copy)'
};
setTracks(prev => prev.map(t => {
if (t.id === drag.trackId) {
const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
id: 'default_' + t.id,
buffer: t.buffer,
startTime: t.startTime || 0,
name: t.name
}] : [];
return {
...t,
clips: [...clips, clone],
buffer: clips.length > 0 ? clips[0].buffer : drag.buffer
};
}
return t;
}));
setDraggedClip({
...drag,
clipId: cloneId,
isDuplicate: false
});
}
const afterSnap = captureTrackSnapshotRef.current(drag.trackId);
pushAction('MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
pushAction(drag.isDuplicate ? 'DUPLICATE_CLIP' : 'MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
setDraggedClip(null);
showToast('Đã di chuyển clip.', 'success');
showToast(drag.isDuplicate ? 'Đã sao chép clip.' : 'Đã di chuyển clip.', 'success');
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
@@ -6075,6 +6116,19 @@ const App = () => {
return next;
});
};
const updateClipName = (trackId, clipId, newName) => {
setTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
const clips = t.clips && t.clips.length > 0 ? t.clips : [];
return {
...t,
clips: clips.map(c => c.id === clipId ? {
...c,
name: newName
} : c)
};
}));
};
// ── Load File on Track (with server upload) ──
const loadFileOnTrack = async (trackId, file) => {
@@ -7296,7 +7350,7 @@ const App = () => {
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = 0;
const clipId = 'clip_' + Date.now();
const clipId = nextClipId();
setTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
@@ -7764,10 +7818,12 @@ const App = () => {
selectedProviderId
};
useEffect(() => {
if (!currentUser) return;
const prefs = prefsRef.current;
localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
if (!currentUser || currentUser === 'cached') return;
const timer = setTimeout(async () => {
try {
await window.SonicAPI.savePreferences(prefsRef.current);
await window.SonicAPI.savePreferences(prefs);
} catch (e) {}
}, 2000);
return () => clearTimeout(timer);
@@ -8382,7 +8438,7 @@ const App = () => {
}), /*#__PURE__*/React.createElement("button", {
onClick: () => setIsLoopingSelection(prev => !prev),
className: `w-7 h-7 flex items-center justify-center rounded border transition ${isLoopingSelection ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
title: "Bật/Tắt Lặp vùng chọn"
title: isLoopingSelection ? selLeft !== null && selRight !== null ? "Loop vùng chọn" : "Loop timeline" : "Bật loop"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
@@ -9178,7 +9234,39 @@ const App = () => {
setSelectionEnd(end);
showToast(`Selected: ${c.name || track.name}`, 'info');
}
}, c.name || track.name))), /*#__PURE__*/React.createElement("div", {
}, c.name || track.name), editingClipName && editingClipName.trackId === track.id && editingClipName.clipId === c.id ? /*#__PURE__*/React.createElement("input", {
type: "text",
value: editNameInput,
autoFocus: true,
onChange: e => setEditNameInput(e.target.value),
onBlur: () => {
if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim());
setEditingClipName(null);
},
onKeyDown: e => {
if (e.key === 'Enter') {
if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim());
setEditingClipName(null);
}
if (e.key === 'Escape') setEditingClipName(null);
},
onClick: e => e.stopPropagation(),
className: "w-20 text-[9px] font-mono bg-black text-cyan-300 border border-cyan-500 rounded px-0.5 py-0 outline-none"
}) : /*#__PURE__*/React.createElement("button", {
className: "text-[9px] text-zinc-600 hover:text-cyan-400 ml-0.5 shrink-0",
title: "Sửa tên clip",
onClick: e => {
e.stopPropagation();
setEditingClipName({
trackId: track.id,
clipId: c.id
});
setEditNameInput(c.name || track.name);
}
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "pencil",
className: "w-2.5 h-2.5"
})))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: e => {
Binary file not shown.