fix: tự động dịch chuyển thanh cuộn xuống dưới cùng

This commit is contained in:
2026-07-22 17:49:08 +07:00
parent 610c384bca
commit b9c524230d
4 changed files with 93 additions and 20 deletions
+49 -9
View File
@@ -3133,6 +3133,12 @@ const App = () => {
const [aiProvider, setAiProvider] = useState('OpenAI');
const [aiModel, setAiModel] = useState('GPT-4o');
const [aiActionLog, setAiActionLog] = useState([]);
const actionLogContainerRef = useRef(null);
useEffect(() => {
if (actionLogContainerRef.current) {
actionLogContainerRef.current.scrollTop = actionLogContainerRef.current.scrollHeight;
}
}, [aiActionLog]);
const [aiProcessing, setAiProcessing] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState('');
const [exportSettings, setExportSettings] = useState({
@@ -7937,25 +7943,58 @@ const App = () => {
exportAudio: async args => {
const tid = args.track_id || selectedTrackId;
const track = tid && tracks.find(t => t.id === tid);
if (!track || !track.buffer) return {
if (!track) return {
success: false,
error: 'No track or audio data'
error: 'No track found'
};
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
id: 'default_' + track.id,
buffer: track.buffer,
startTime: track.startTime || 0,
name: track.name
}] : [];
if (clips.length === 0) return {
success: false,
error: 'Track has no audio clips'
};
const totalDuration = Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 0)));
const barDur = 60 / parseInt(bpm || 120) * 4;
const sel = selectionRef.current;
let rawStart, rawEnd;
if (args.start_time !== undefined) rawStart = args.start_time;else if (args.start_bar !== undefined) rawStart = args.start_bar * barDur;else if (sel.start !== null) rawStart = sel.start;else rawStart = 0;
if (args.end_time !== undefined) rawEnd = args.end_time;else if (args.length_bars !== undefined) rawEnd = (rawStart || 0) + args.length_bars * barDur;else if (args.end_bar !== undefined) rawEnd = args.end_bar * barDur;else if (sel.end !== null && sel.end > rawStart) rawEnd = sel.end;else rawEnd = track.buffer.duration;
if (args.end_time !== undefined) rawEnd = args.end_time;else if (args.length_bars !== undefined) rawEnd = (rawStart || 0) + args.length_bars * barDur;else if (args.end_bar !== undefined) rawEnd = args.end_bar * barDur;else if (sel.end !== null && sel.end > rawStart) rawEnd = sel.end;else rawEnd = totalDuration;
if (rawEnd <= rawStart) return {
success: false,
error: 'Export range is empty or invalid.'
};
const ctx = getAudioContext();
const sr = parseInt(args.sample_rate || '44100');
const numCh = args.channels === 'mono' ? 1 : track.buffer.numberOfChannels || 2;
const firstBuffer = clips.find(c => c.buffer)?.buffer;
const numCh = args.channels === 'mono' ? 1 : firstBuffer ? firstBuffer.numberOfChannels : 2;
const bd = parseInt(args.bit_depth || '16');
const fmt = args.format || 'wav';
const offlineCtx = new OfflineAudioContext(numCh, Math.ceil(sr * Math.min(rawEnd - rawStart, track.buffer.duration)), sr);
const source = offlineCtx.createBufferSource();
source.buffer = track.buffer;
source.start(0, rawStart, rawEnd - rawStart);
source.connect(offlineCtx.destination);
const renderLength = rawEnd - rawStart;
const offlineCtx = new OfflineAudioContext(numCh, Math.ceil(sr * renderLength), sr);
clips.forEach(clip => {
if (!clip.buffer) return;
const clipStart = clip.startTime || 0;
const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
const clipEnd = clipStart + clipDuration;
if (clipEnd <= rawStart || clipStart >= rawEnd) return;
const source = offlineCtx.createBufferSource();
source.buffer = clip.buffer;
source.playbackRate.value = clip.speed || 1.0;
source.connect(offlineCtx.destination);
if (rawStart < clipStart) {
const delay = clipStart - rawStart;
const playDur = Math.min(clipDuration, rawEnd - clipStart);
source.start(delay, 0, playDur * (clip.speed || 1.0));
} else {
const offsetInClip = rawStart - clipStart;
const playDur = Math.min(clipEnd - rawStart, renderLength);
source.start(0, offsetInClip * (clip.speed || 1.0), playDur * (clip.speed || 1.0));
}
});
const renderedBuffer = await offlineCtx.startRendering();
const len = renderedBuffer.length;
const bps = bd / 8;
@@ -9279,6 +9318,7 @@ const App = () => {
"data-lucide": "list",
className: "w-3 h-3"
}), " Action Log")), /*#__PURE__*/React.createElement("div", {
ref: actionLogContainerRef,
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"