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
+37 -8
View File
@@ -3090,6 +3090,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({
@@ -7627,7 +7633,10 @@ 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 { success: false, error: 'No track or audio data' };
if (!track) return { success: false, 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;
@@ -7639,17 +7648,36 @@ const App = () => {
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;
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;
@@ -8784,6 +8812,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"
+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"
+7 -3
View File
@@ -41,11 +41,11 @@ const AIGateway = (function() {
}, {
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: 'fade_in', description: 'Fade-in clip (0.5s đến max)', parameters: { type: 'object', properties: { track_id: { type: 'string' }, duration_seconds: { type: 'number' } } }
name: 'fade_in', description: 'Fade-in clip (0.5s đến max)', parameters: { type: 'object', properties: { track_id: { type: 'string' }, duration_seconds: { type: 'number' }, clip_index: { type: 'number', description: 'Chỉ số của clip trên track (1-based, ví dụ: 1 cho clip 1, 2 cho clip 2)' }, clip_id: { type: 'string', description: 'ID của clip cụ thể' } } }
}, {
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' } } }
name: 'fade_out', description: 'Fade-out clip (0.5s đến max)', parameters: { type: 'object', properties: { track_id: { type: 'string' }, duration_seconds: { type: 'number' }, clip_index: { type: 'number', description: 'Chỉ số của clip trên track (1-based, ví dụ: 1 cho clip 1, 2 cho clip 2)' }, clip_id: { type: 'string', description: 'ID của clip cụ thể' } } }
}];
function parseOrigin(urlStr) {
@@ -148,7 +148,11 @@ Nhiệm vụ của bạn là phân tích yêu cầu của người dùng và chu
QUAN TRỌNG:
1. Bạn đang hoạt động ở chế độ một lượt (one-shot). Hãy trả về TẤT CẢ các function calls cần thiết để thực hiện toàn bộ các bước trong yêu cầu của người dùng trong một phản hồi duy nhất. Đừng thực hiện từng bước qua nhiều lượt chat.
2. Có thể gọi nhiều function cùng một lúc (gọi song song/nối tiếp). Chúng sẽ được thực thi theo thứ tự bạn trả về.
3. Khi một lệnh trước đó (như 'cut_audio') tạo ra một track mới, track đó sẽ tự động được chọn làm active track (selectedTrackId). Do đó, đối với các lệnh tiếp theo áp dụng lên track mới này (như 'fade_in', 'set_track_volume'), bạn hãy để trống tham số 'track_id' (hoặc truyền null) để hệ thống tự động áp dụng lên track vừa được tạo và chọn.
3. Khi người dùng yêu cầu chọn và cắt/sao chép/copy một đoạn nhạc từ track cũ để tạo đoạn nhạc mới (bằng lệnh 'cut_audio'), và sau đó yêu cầu xử lý tiếp đoạn nhạc mới tạo đó (ví dụ: 'sau đó fade in đoạn đó', 'chỉnh âm lượng đoạn đó', 'xuất mp3 đoạn đó'...), thì tất cả các lệnh xử lý tiếp theo này (như 'fade_in', 'export_audio', 'set_track_volume') PHẢI để trống tham số 'track_id' (hoặc truyền null/không truyền) để hệ thống tự động áp dụng lên track mới vừa được tạo ra. KHÔNG ĐƯỢC dùng 'track_id' của track gốc ban đầu cho các lệnh xử lý phía sau.
Ví dụ: "Hãy chọn và copy từ bar 4 đến bar 12 của track 1 sau đó fade in clip đó 3s, xuất ra mp3" -> Bạn phải trả về đồng thời 3 cuộc gọi hàm theo thứ tự:
- cut_audio({"track_id": "1", "start_bar": 4, "end_bar": 12})
- fade_in({"duration_seconds": 3}) (không truyền track_id)
- export_audio({"format": "mp3"}) (không truyền track_id)
4. Bar 0 đại diện cho bar đầu tiên trên timeline.` },
{ role: 'user', content: `Ngữ cảnh DAW hiện tại:\n${contextStr}\n\nYêu cầu người dùng: ${prompt}` }
];