Skip to content

Implemented new Studio UI#627

Merged
ChuxiJ merged 1 commit intoace-step:mainfrom
goedzo:main
Feb 18, 2026
Merged

Implemented new Studio UI#627
ChuxiJ merged 1 commit intoace-step:mainfrom
goedzo:main

Conversation

@goedzo
Copy link
Contributor

@goedzo goedzo commented Feb 17, 2026

I took the liberty to use all my API enhancement to update the studio UI to a much nice version

ace-step-ui1

Summary by CodeRabbit

  • New Features
    • Completely redesigned studio interface with a two-pane layout for improved workflow
    • Added audio waveform visualization with interactive region selection
    • Multiple task modes including text-to-music, cover, repaint, and more
    • Audio upload and reference playback functionality
    • Real-time progress tracking with visual progress bar
    • Built-in audio player with download support
    • Modern dark theme with improved responsive design

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This pull request completely redesigns the ACE-Step Studio HTML interface. The changes introduce a two-pane layout with a controls sidebar and interactive content area, integrate WaveSurfer for audio waveform visualization, implement an audio upload workflow with region-based editing, and add a task submission system with real-time progress tracking and result polling. The UI transitions to a dark theme with a dynamic, mode-driven configuration that supports multiple task types. Approximately 797 lines added and 199 removed in a single, comprehensive rewrite.

Changes

Cohort / File(s) Summary
WaveSurfer Integration & Audio Visualization
ui/studio.html
Added WaveSurfer library with Regions plugin for reference audio waveform display, region selection/management, playback controls, and visual region boundaries. Supports region-based painting/repainting for applicable task modes.
Audio Upload Workflow & Task Configuration
ui/studio.html
Introduced complete audio file upload mechanism with waveform initialization. Implemented dynamic task mode system (text-to-music, cover, repaint, lego, extract, complete) with conditional UI sections for track selection, style/description, lyrics, language, duration, steps, guidance, and inference parameters. Region data submission integrated into FormData payload.
API Integration & Result Handling
ui/studio.html
Implemented task submission via release_task endpoint with FormData payload including task type, prompts, audio file, and region metadata. Added polling mechanism via query_result to monitor task status and trigger progress updates. Renders result card with audio player and download link on success; logs errors and re-enables submission on failure.
Smart Progress Mechanism & Client-Side State
ui/studio.html
Added comprehensive progress tracking system with smooth progress bar animations driven by log-phrase-to-percentage mappings. Implemented state management for UI elements (upload handlers, task configuration changes, play/pause, loop, region updates). Integrated client-side logging console and enhanced error handling with user feedback.
Dark Theme & Layout Redesign
ui/studio.html
Overhauled styling with dark theme and CSS variables for consistent design. Implemented responsive two-pane layout (left sidebar controls, right content area), header component, reusable sections, and visual improvements to component hierarchy and spacing.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Client<br/>(UI/WaveSurfer)
    participant Server
    
    User->>UI: Upload reference audio
    activate UI
    UI->>UI: Initialize WaveSurfer<br/>Render waveform
    deactivate UI
    
    User->>UI: Select task mode &<br/>configure parameters<br/>(regions, prompts, etc.)
    activate UI
    UI->>UI: Update conditional<br/>UI sections
    deactivate UI
    
    User->>UI: Submit task
    activate UI
    UI->>UI: Start progress bar
    UI->>Server: release_task<br/>(FormData: task config,<br/>audio, regions)
    deactivate UI
    
    activate Server
    Server-->>UI: Task accepted
    deactivate Server
    
    activate UI
    loop Poll Status
        UI->>Server: query_result (task_id)
        activate Server
        Server-->>UI: Task status &<br/>progress logs
        deactivate Server
        UI->>UI: Update progress bar<br/>via log mapping
    end
    
    Server-->>UI: Task complete<br/>(output audio)
    deactivate UI
    
    activate UI
    UI->>UI: Render result card<br/>with player & download
    UI->>User: Display results
    deactivate UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🎵 The waveform dances, oh what delight!
Audio flows in colors so bright,
Tasks take their form in sidebars neat,
Progress marches to a steady beat,
Studio Pro now hops with might! 🐰✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implemented new Studio UI' accurately summarizes the main change: a comprehensive redesign of the Studio UI with new layout, features, and styling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
ui/studio.html (2)

438-442: Revoke object URLs on re-upload/unload to avoid leaks.

URL.createObjectURL should be revoked when replaced or on unload to prevent memory growth during repeated uploads.

♻️ Proposed fix
 let wavesurfer = null;
 let wsRegions = null;
 let activeRegion = null;
 let uploadedFile = null;
+let currentObjectUrl = null;
 
 elements.fileInput.addEventListener('change', function(e) {
     const file = e.target.files[0];
     if (!file) return;
     uploadedFile = file;
     elements.uploadLabel.innerText = "✅ " + file.name;
     elements.uploadLabel.style.borderColor = "var(--accent)";
     elements.uploadLabel.style.color = "var(--accent)";
     elements.waveformWrapper.classList.remove('empty');
     initWaveSurfer();
-    wavesurfer.load(URL.createObjectURL(file));
+    if (currentObjectUrl) URL.revokeObjectURL(currentObjectUrl);
+    currentObjectUrl = URL.createObjectURL(file);
+    wavesurfer.load(currentObjectUrl);
 });
+
+window.addEventListener('beforeunload', () => {
+    if (currentObjectUrl) URL.revokeObjectURL(currentObjectUrl);
+});

Also applies to: 575-585

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/studio.html` around lines 438 - 442, The file creates object URLs for
uploads but never revokes them, causing leaks; update the upload handler (where
uploadedFile is set) to call URL.revokeObjectURL(uploadedFile) before assigning
a new uploadedFile and revoking any previous region-related blobs, and add a
window unload handler (or existing cleanup function) to revoke uploadedFile and
any region blobs (reference variables uploadedFile, activeRegion, wsRegions, and
wavesurfer) so all created object URLs are revoked when replaced or on page
unload.

573-573: Remove or implement the no-op resize listener.

The current handler schedules an empty callback; either drop it or wire it to real resize handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/studio.html` at line 573, The resize listener is a no-op; either remove
the window.addEventListener('resize', ...) line or replace the empty setTimeout
with a real resize/redraw call for the Wavesurfer instance: detect the
wavesurfer symbol and invoke its resize/redraw API (for example, call
wavesurfer.drawBuffer() or
wavesurfer.drawer.resize()/wavesurfer.params.container rerender logic depending
on your Wavesurfer version) and guard with if (wavesurfer) to avoid errors;
ensure the handler triggers the UI redraw logic instead of scheduling an empty
callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/studio.html`:
- Around line 670-715: Before appending repainted region values in the submit
handler, parse elements.regionStart.value and elements.regionEnd.value to
numbers and validate that for taskType 'repaint' or 'lego' the end is >= 0 and
end > start; if the check fails, show an alert (or set
elements.submitBtn.disabled=false and return) to stop submission. Only append
'repainting_start' and 'repainting_end' to formData after the numeric validation
succeeds, and ensure you append numeric values (not raw strings) so backend
receives a valid range.
- Around line 738-783: The current startPolling uses setInterval which can
trigger overlapping async fetches; replace it with a recursive setTimeout loop
inside startPolling that awaits each fetch before scheduling the next timeout
(use a single boolean like "isPolling" or simply chain the next setTimeout call
after processing) to ensure only one in-flight request at a time, preserve the
existing pollInterval and maxTime logic (check Date.now() - startTime and
clear/stop when timeout), keep the same behavior on success/failure by calling
stopSmartProgressBar(), log messages, updateProgressTarget(task.progress_text),
handleSuccess(task) and re-enable elements.submitBtn, and ensure errors are
caught and then the next poll is scheduled (or polling stopped on terminal
states).
- Around line 785-812: The handleSuccess function currently uses card.innerHTML
to inject result.prompt and result.metas (and the URL) which enables DOM XSS;
modify handleSuccess to build the result-card DOM using createElement and
setting textContent for any user/server-provided text (e.g., result.prompt,
metas.bpm/keyscale/duration, elements.prompt fallback) instead of interpolating
into innerHTML, and validate/sanitize fileUrl/fullUrl before assigning to
media/anchor by ensuring the scheme is http or https (reject or neutralize other
schemes), then set audio.src and anchor.href via properties (not via innerHTML)
and append the constructed nodes to card. Ensure you reference handleSuccess,
elements.prompt, result.prompt, result.metas, fileUrl/fullUrl, and getBaseUrl
when making the changes.
- Around line 8-9: Update the floating version script tags to pin to the exact
WaveSurfer.js release and add Subresource Integrity and crossorigin attributes:
replace the src values referencing "https://unpkg.com/wavesurfer.js@7" with the
explicit CDN paths for the released artifacts (e.g.,
"https://unpkg.com/wavesurfer.js@7.12.1/dist/wavesurfer.min.js" and the plugin
"https://unpkg.com/wavesurfer.js@7.12.1/dist/plugins/regions.min.js"), add
integrity="sha384-..." and crossorigin="anonymous" attributes on both <script>
tags, and fetch the correct SRI hashes from unpkg's metadata API by appending
?meta to each package URL before committing.

---

Nitpick comments:
In `@ui/studio.html`:
- Around line 438-442: The file creates object URLs for uploads but never
revokes them, causing leaks; update the upload handler (where uploadedFile is
set) to call URL.revokeObjectURL(uploadedFile) before assigning a new
uploadedFile and revoking any previous region-related blobs, and add a window
unload handler (or existing cleanup function) to revoke uploadedFile and any
region blobs (reference variables uploadedFile, activeRegion, wsRegions, and
wavesurfer) so all created object URLs are revoked when replaced or on page
unload.
- Line 573: The resize listener is a no-op; either remove the
window.addEventListener('resize', ...) line or replace the empty setTimeout with
a real resize/redraw call for the Wavesurfer instance: detect the wavesurfer
symbol and invoke its resize/redraw API (for example, call
wavesurfer.drawBuffer() or
wavesurfer.drawer.resize()/wavesurfer.params.container rerender logic depending
on your Wavesurfer version) and guard with if (wavesurfer) to avoid errors;
ensure the handler triggers the UI redraw logic instead of scheduling an empty
callback.

Comment on lines +8 to +9
<script src="https://unpkg.com/wavesurfer.js@7"></script>
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

WaveSurfer.js 7.x latest stable version npm release


The latest stable 7.x npm release of wavesurfer.js is 7.12.1 (published 2025-12-04/05). [1][2][3]

npm i wavesurfer.js@^7.12.1

Sources: jsDelivr package metadata [1], WebJars/NPM version list [2], SourceForge release mirror notes/timestamp [3].


WaveSurfer.js official documentation CDN SRI integrity crossorigin


WaveSurfer.js official docs show CDN usage via UNPKG (pinned version in the URL), e.g. plugin script tags like: https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js—but they do not provide SRI (integrity) hashes or crossorigin examples in the docs. [1]

How to use WaveSurfer.js from a CDN with SRI + crossorigin

  1. Pin an exact version and a specific file (no “latest”/range URLs).
  2. Compute an SRI hash for that exact URL/file, then add:
  • integrity="sha384-..."
  • crossorigin="anonymous" (required for SRI checks on cross-origin resources) [2][3]

Example (you must replace PASTE_SHA384_BASE64_HERE with the real hash for the exact file you load):

<script
  src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"
  integrity="sha384-PASTE_SHA384_BASE64_HERE"
  crossorigin="anonymous"></script>

Notes if you use jsDelivr instead of UNPKG

jsDelivr warns to only use SRI with “static, single-file links” and not with version aliasing, default files, or on-demand minified/combined files (they can change byte-for-byte). [4]

Sources: WaveSurfer docs CDN examples [1]; W3C SRI usage (integrity + crossorigin) [2]; why crossorigin="anonymous" matters for SRI [3]; jsDelivr SRI guidance [4].


unpkg CDN version pinning best practices security SRI


unpkg CDN: version pinning + security (SRI) best practices

  1. Pin exact versions (avoid latest and semver ranges like ^18)
    unpkg lets you use npm tags and semver ranges, but those can resolve to different code over time and will redirect to a fully-resolved version. For stability and cache hits, use full pkg@x.y.z + full file path. [1]

  2. Prefer “permanent URLs” (exact version) for caching + immutability assumptions
    unpkg caches based on the permanent URL containing the package version, relying on npm’s rule that already-published versions can’t be overwritten. [1]

  3. Always use SRI for third-party scripts/styles
    Use the HTML integrity attribute so the browser refuses to execute a tampered resource. (SRI supports sha256, sha384, sha512.) [2][3]

  4. Get SRI hashes from unpkg’s Metadata API (?meta)
    Appending ?meta to a package directory URL returns JSON metadata including each file’s integrity value. [1] (Example noted here too: ...react@16.7.0/...js?meta.) [4]

  5. Include crossorigin="anonymous" when using SRI with cross-origin resources
    This is part of “correct SRI” usage for CDN-loaded <script>/<link> in common tooling/guidance. [5]

  6. Treat SRI + pinning as “update requires a deliberate change”
    If you upgrade the library version, you must update the URL and the SRI hash, otherwise it will fail to load. [3]

Example (pinned + SRI):

<script
  src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"
  integrity="sha256-…"
  crossorigin="anonymous">
</script>

Sources: unpkg docs (versioning, redirects, ?meta integrity) [1]; W3C SRI spec [2]; OWASP SRI overview [3]; unpkg ?meta usage example [4]; webhint SRI correctness checks (incl. crossorigin + recommended hash strength) [5].


Pin exact WaveSurfer.js version and add SRI integrity hash.

The floating @7 tag resolves to different versions over time, bypassing cache immutability and increasing supply-chain risk. Pin to the latest stable version (currently 7.12.1) and add integrity + crossorigin="anonymous" attributes. Retrieve SRI hashes from unpkg's metadata API by appending ?meta to the package URL.

Example:

<script src="https://unpkg.com/wavesurfer.js@7.12.1/dist/wavesurfer.min.js" integrity="sha384-[hash]" crossorigin="anonymous"></script>
<script src="https://unpkg.com/wavesurfer.js@7.12.1/dist/plugins/regions.min.js" integrity="sha384-[hash]" crossorigin="anonymous"></script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/studio.html` around lines 8 - 9, Update the floating version script tags
to pin to the exact WaveSurfer.js release and add Subresource Integrity and
crossorigin attributes: replace the src values referencing
"https://unpkg.com/wavesurfer.js@7" with the explicit CDN paths for the released
artifacts (e.g., "https://unpkg.com/wavesurfer.js@7.12.1/dist/wavesurfer.min.js"
and the plugin
"https://unpkg.com/wavesurfer.js@7.12.1/dist/plugins/regions.min.js"), add
integrity="sha384-..." and crossorigin="anonymous" attributes on both <script>
tags, and fetch the correct SRI hashes from unpkg's metadata API by appending
?meta to each package URL before committing.

Comment on lines +670 to +715
elements.submitBtn.addEventListener('click', async () => {
const taskType = elements.taskType.value;
if (taskType !== 'text2music' && !uploadedFile) {
alert("Upload an audio file first!");
return;
}

elements.submitBtn.disabled = true;
elements.progressContainer.style.display = 'block';
elements.logConsole.innerHTML = '';
log("Preparing task...", "log-info");

// Start the smart animator
startSmartProgressBar();

try {
const formData = new FormData();
formData.append('task_type', taskType);
formData.append('prompt', elements.prompt.value);
formData.append('lyrics', elements.lyrics.value);
formData.append('vocal_language', elements.language.value);
formData.append('thinking', elements.thinking.checked);
formData.append('batch_size', 1);
formData.append('inference_steps', elements.steps.value);
formData.append('guidance_scale', elements.guidance.value);
formData.append('audio_duration', elements.duration.value);

if (taskType === 'cover' || taskType === 'lego') {
formData.append('audio_cover_strength', elements.strength.value / 100);
} else {
formData.append('audio_cover_strength', 1.0);
}

if (taskType === 'lego' || taskType === 'extract') {
formData.append('track_name', elements.trackName.value);
}

if (uploadedFile) {
formData.append('src_audio', uploadedFile);
}

if (taskType === 'repaint' || taskType === 'lego') {
formData.append('repainting_start', elements.regionStart.value);
formData.append('repainting_end', elements.regionEnd.value);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate region selection for repaint/lego before submission.

regionEnd defaults to -1, so repaint/lego can submit an invalid range. This can trigger backend errors or undefined output.

🐛 Proposed fix
 elements.submitBtn.addEventListener('click', async () => {
     const taskType = elements.taskType.value;
     if (taskType !== 'text2music' && !uploadedFile) {
         alert("Upload an audio file first!");
         return;
     }
+    if (taskType === 'repaint' || taskType === 'lego') {
+        const start = Number(elements.regionStart.value);
+        const end = Number(elements.regionEnd.value);
+        if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
+            alert("Select a valid region before submitting.");
+            return;
+        }
+    }
 
     elements.submitBtn.disabled = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/studio.html` around lines 670 - 715, Before appending repainted region
values in the submit handler, parse elements.regionStart.value and
elements.regionEnd.value to numbers and validate that for taskType 'repaint' or
'lego' the end is >= 0 and end > start; if the check fails, show an alert (or
set elements.submitBtn.disabled=false and return) to stop submission. Only
append 'repainting_start' and 'repainting_end' to formData after the numeric
validation succeeds, and ensure you append numeric values (not raw strings) so
backend receives a valid range.

Comment on lines +738 to +783
async function startPolling(taskId) {
const pollInterval = 1500;
const maxWait = 600000;
const start = Date.now();
let list;

while (Date.now() - start < maxWait) {
await new Promise(function (r) { setTimeout(r, pollInterval); });
list = await queryResult([taskId]);
if (!list || !list.length) { log('No result in response'); continue; }
const item = list[0];
const status = item.status;
if (item.progress_text) log(item.progress_text);
if (status === 1) {
log('Done.', 'success');
const maxTime = 900000; // 15 mins (increased for diffusion)
const startTime = Date.now();

const timer = setInterval(async () => {
if (Date.now() - startTime > maxTime) {
clearInterval(timer);
log("Timed out.", "log-error");
stopSmartProgressBar();
elements.submitBtn.disabled = false;
return;
}

try {
const result = typeof item.result === 'string' ? JSON.parse(item.result) : item.result;
const arr = Array.isArray(result) ? result : [result];
arr.forEach(function (r) { renderResult(taskId, r, base); });
const res = await fetch(`${getBaseUrl()}/query_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id_list: [taskId] })
});
const data = await res.json();

if (data.code === 200 && data.data && data.data.length > 0) {
const task = data.data[0];

// Update Smart Target
updateProgressTarget(task.progress_text);

if (task.status === 1) { // Success
clearInterval(timer);
stopSmartProgressBar(); // Force to 100%
log("Done!", "log-success");
handleSuccess(task);
elements.submitBtn.disabled = false;
} else if (task.status === 2) { // Failed
clearInterval(timer);
stopSmartProgressBar();
log("Task Failed.", "log-error");
elements.submitBtn.disabled = false;
}
}
} catch (e) {
log('Parse result: ' + e.message, 'error');
console.error("Poll error", e);
}
break;
}
if (status === 2) {
log('Task failed.', 'error');
break;
}
}
if (Date.now() - start >= maxWait) log('Timeout.', 'error');
} catch (e) {
log(e.message || 'Error', 'error');
}
submitBtn.disabled = false;
});
</script>
}, pollInterval);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and check the file
find . -name "studio.html" -type f

Repository: ace-step/ACE-Step-1.5

Length of output: 81


🏁 Script executed:

# Check line count and read the specific lines
sed -n '738,783p' ./ui/studio.html

Repository: ace-step/ACE-Step-1.5

Length of output: 1914


Replace setInterval with recursive setTimeout to prevent overlapping polls.

The current code uses setInterval(async () => {...}, 1500), which allows multiple requests to be in-flight simultaneously if the fetch and processing takes longer than 1500ms. This causes responses to potentially arrive out of order and creates race conditions. Use a recursive setTimeout pattern with a flag to ensure only one poll is active at a time.

🐛 Proposed fix
 async function startPolling(taskId) {
     const pollInterval = 1500;
     const maxTime = 900000; // 15 mins (increased for diffusion)
     const startTime = Date.now();
-
-    const timer = setInterval(async () => {
-        if (Date.now() - startTime > maxTime) {
-            clearInterval(timer);
-            log("Timed out.", "log-error");
-            stopSmartProgressBar();
-            elements.submitBtn.disabled = false;
-            return;
-        }
-
-        try {
-            const res = await fetch(`${getBaseUrl()}/query_result`, {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ task_id_list: [taskId] })
-            });
-            const data = await res.json();
-
-            if (data.code === 200 && data.data && data.data.length > 0) {
-                const task = data.data[0];
-                
-                // Update Smart Target
-                updateProgressTarget(task.progress_text);
-
-                if (task.status === 1) { // Success
-                    clearInterval(timer);
-                    stopSmartProgressBar(); // Force to 100%
-                    log("Done!", "log-success");
-                    handleSuccess(task);
-                    elements.submitBtn.disabled = false;
-                } else if (task.status === 2) { // Failed
-                    clearInterval(timer);
-                    stopSmartProgressBar();
-                    log("Task Failed.", "log-error");
-                    elements.submitBtn.disabled = false;
-                }
-            }
-        } catch (e) {
-            console.error("Poll error", e);
-        }
-    }, pollInterval);
+    let stopped = false;
+
+    const poll = async () => {
+        if (stopped) return;
+        if (Date.now() - startTime > maxTime) {
+            stopped = true;
+            log("Timed out.", "log-error");
+            stopSmartProgressBar();
+            elements.submitBtn.disabled = false;
+            return;
+        }
+
+        try {
+            const res = await fetch(`${getBaseUrl()}/query_result`, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ task_id_list: [taskId] })
+            });
+            const data = await res.json();
+
+            if (data.code === 200 && data.data && data.data.length > 0) {
+                const task = data.data[0];
+                
+                // Update Smart Target
+                updateProgressTarget(task.progress_text);
+
+                if (task.status === 1) { // Success
+                    stopped = true;
+                    stopSmartProgressBar(); // Force to 100%
+                    log("Done!", "log-success");
+                    handleSuccess(task);
+                    elements.submitBtn.disabled = false;
+                    return;
+                } else if (task.status === 2) { // Failed
+                    stopped = true;
+                    stopSmartProgressBar();
+                    log("Task Failed.", "log-error");
+                    elements.submitBtn.disabled = false;
+                    return;
+                }
+            }
+        } catch (e) {
+            console.error("Poll error", e);
+        }
+
+        if (!stopped) setTimeout(poll, pollInterval);
+    };
+
+    poll();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function startPolling(taskId) {
const pollInterval = 1500;
const maxWait = 600000;
const start = Date.now();
let list;
while (Date.now() - start < maxWait) {
await new Promise(function (r) { setTimeout(r, pollInterval); });
list = await queryResult([taskId]);
if (!list || !list.length) { log('No result in response'); continue; }
const item = list[0];
const status = item.status;
if (item.progress_text) log(item.progress_text);
if (status === 1) {
log('Done.', 'success');
const maxTime = 900000; // 15 mins (increased for diffusion)
const startTime = Date.now();
const timer = setInterval(async () => {
if (Date.now() - startTime > maxTime) {
clearInterval(timer);
log("Timed out.", "log-error");
stopSmartProgressBar();
elements.submitBtn.disabled = false;
return;
}
try {
const result = typeof item.result === 'string' ? JSON.parse(item.result) : item.result;
const arr = Array.isArray(result) ? result : [result];
arr.forEach(function (r) { renderResult(taskId, r, base); });
const res = await fetch(`${getBaseUrl()}/query_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id_list: [taskId] })
});
const data = await res.json();
if (data.code === 200 && data.data && data.data.length > 0) {
const task = data.data[0];
// Update Smart Target
updateProgressTarget(task.progress_text);
if (task.status === 1) { // Success
clearInterval(timer);
stopSmartProgressBar(); // Force to 100%
log("Done!", "log-success");
handleSuccess(task);
elements.submitBtn.disabled = false;
} else if (task.status === 2) { // Failed
clearInterval(timer);
stopSmartProgressBar();
log("Task Failed.", "log-error");
elements.submitBtn.disabled = false;
}
}
} catch (e) {
log('Parse result: ' + e.message, 'error');
console.error("Poll error", e);
}
break;
}
if (status === 2) {
log('Task failed.', 'error');
break;
}
}
if (Date.now() - start >= maxWait) log('Timeout.', 'error');
} catch (e) {
log(e.message || 'Error', 'error');
}
submitBtn.disabled = false;
});
</script>
}, pollInterval);
}
async function startPolling(taskId) {
const pollInterval = 1500;
const maxTime = 900000; // 15 mins (increased for diffusion)
const startTime = Date.now();
let stopped = false;
const poll = async () => {
if (stopped) return;
if (Date.now() - startTime > maxTime) {
stopped = true;
log("Timed out.", "log-error");
stopSmartProgressBar();
elements.submitBtn.disabled = false;
return;
}
try {
const res = await fetch(`${getBaseUrl()}/query_result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id_list: [taskId] })
});
const data = await res.json();
if (data.code === 200 && data.data && data.data.length > 0) {
const task = data.data[0];
// Update Smart Target
updateProgressTarget(task.progress_text);
if (task.status === 1) { // Success
stopped = true;
stopSmartProgressBar(); // Force to 100%
log("Done!", "log-success");
handleSuccess(task);
elements.submitBtn.disabled = false;
return;
} else if (task.status === 2) { // Failed
stopped = true;
stopSmartProgressBar();
log("Task Failed.", "log-error");
elements.submitBtn.disabled = false;
return;
}
}
} catch (e) {
console.error("Poll error", e);
}
if (!stopped) setTimeout(poll, pollInterval);
};
poll();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/studio.html` around lines 738 - 783, The current startPolling uses
setInterval which can trigger overlapping async fetches; replace it with a
recursive setTimeout loop inside startPolling that awaits each fetch before
scheduling the next timeout (use a single boolean like "isPolling" or simply
chain the next setTimeout call after processing) to ensure only one in-flight
request at a time, preserve the existing pollInterval and maxTime logic (check
Date.now() - startTime and clear/stop when timeout), keep the same behavior on
success/failure by calling stopSmartProgressBar(), log messages,
updateProgressTarget(task.progress_text), handleSuccess(task) and re-enable
elements.submitBtn, and ensure errors are caught and then the next poll is
scheduled (or polling stopped on terminal states).

Comment on lines +785 to +812
function handleSuccess(task) {
try {
let result = task.result;
if (typeof result === 'string') result = JSON.parse(result);
if (Array.isArray(result)) result = result[0];

const baseUrl = getBaseUrl();
const fileUrl = result.url || result.file;
const fullUrl = fileUrl.startsWith('http') ? fileUrl : `${baseUrl}/${fileUrl.replace(/^\//, '')}`;

const card = document.createElement('div');
card.className = 'result-card';

const metas = result.metas || {};
const metaHtml = `
<div class="result-meta">
<strong>${(result.prompt || elements.prompt.value).substring(0, 50)}...</strong><br>
<span>BPM: ${metas.bpm || '-'} | Key: ${metas.keyscale || '-'} | Dur: ${metas.duration || '-'}s</span>
</div>
`;

card.innerHTML = `
${metaHtml}
<audio controls src="${fullUrl}"></audio>
<div style="margin-top:12px; display:flex; gap:10px;">
<a href="${fullUrl}" download class="btn" style="padding:10px; font-size:0.8rem; background:#333; text-decoration:none; color:white; text-align:center;">Download</a>
</div>
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Prevent DOM XSS in result rendering.

innerHTML interpolates result.prompt/result.metas which can contain HTML from the server (or user input), enabling DOM XSS. Build DOM nodes with textContent and validate URL schemes.

🛡️ Proposed fix
+function safeMediaUrl(rawUrl, baseUrl) {
+    const url = new URL(rawUrl, baseUrl);
+    if (!['http:', 'https:'].includes(url.protocol)) {
+        throw new Error('Unsupported media URL');
+    }
+    return url.href;
+}
+
 function handleSuccess(task) {
     try {
         let result = task.result;
         if (typeof result === 'string') result = JSON.parse(result);
         if (Array.isArray(result)) result = result[0];
 
         const baseUrl = getBaseUrl();
         const fileUrl = result.url || result.file;
-        const fullUrl = fileUrl.startsWith('http') ? fileUrl : `${baseUrl}/${fileUrl.replace(/^\//, '')}`;
-
-        const card = document.createElement('div');
-        card.className = 'result-card';
-        
-        const metas = result.metas || {};
-        const metaHtml = `
-            <div class="result-meta">
-                <strong>${(result.prompt || elements.prompt.value).substring(0, 50)}...</strong><br>
-                <span>BPM: ${metas.bpm || '-'} | Key: ${metas.keyscale || '-'} | Dur: ${metas.duration || '-'}s</span>
-            </div>
-        `;
-
-        card.innerHTML = `
-            ${metaHtml}
-            <audio controls src="${fullUrl}"></audio>
-            <div style="margin-top:12px; display:flex; gap:10px;">
-                <a href="${fullUrl}" download class="btn" style="padding:10px; font-size:0.8rem; background:`#333`; text-decoration:none; color:white; text-align:center;">Download</a>
-            </div>
-        `;
+        const fullUrl = safeMediaUrl(
+            fileUrl.startsWith('http') ? fileUrl : `${baseUrl}/${fileUrl.replace(/^\//, '')}`,
+            baseUrl
+        );
+
+        const card = document.createElement('div');
+        card.className = 'result-card';
+        
+        const metas = result.metas || {};
+        const metaDiv = document.createElement('div');
+        metaDiv.className = 'result-meta';
+        const title = document.createElement('strong');
+        title.textContent = `${(result.prompt || elements.prompt.value).substring(0, 50)}...`;
+        const details = document.createElement('span');
+        details.textContent = `BPM: ${metas.bpm || '-'} | Key: ${metas.keyscale || '-'} | Dur: ${metas.duration || '-'}s`;
+        metaDiv.append(title, document.createElement('br'), details);
+
+        const audio = document.createElement('audio');
+        audio.controls = true;
+        audio.src = fullUrl;
+
+        const actions = document.createElement('div');
+        actions.style.marginTop = '12px';
+        actions.style.display = 'flex';
+        actions.style.gap = '10px';
+        const download = document.createElement('a');
+        download.href = fullUrl;
+        download.download = '';
+        download.className = 'btn';
+        download.style.padding = '10px';
+        download.style.fontSize = '0.8rem';
+        download.style.background = '#333';
+        download.style.textDecoration = 'none';
+        download.style.color = 'white';
+        download.style.textAlign = 'center';
+        download.textContent = 'Download';
+        actions.appendChild(download);
+
+        card.append(metaDiv, audio, actions);
 
         elements.resultsGrid.prepend(card);
     } catch (e) {
         log("Parsing error: " + e.message, "log-error");
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/studio.html` around lines 785 - 812, The handleSuccess function currently
uses card.innerHTML to inject result.prompt and result.metas (and the URL) which
enables DOM XSS; modify handleSuccess to build the result-card DOM using
createElement and setting textContent for any user/server-provided text (e.g.,
result.prompt, metas.bpm/keyscale/duration, elements.prompt fallback) instead of
interpolating into innerHTML, and validate/sanitize fileUrl/fullUrl before
assigning to media/anchor by ensuring the scheme is http or https (reject or
neutralize other schemes), then set audio.src and anchor.href via properties
(not via innerHTML) and append the constructed nodes to card. Ensure you
reference handleSuccess, elements.prompt, result.prompt, result.metas,
fileUrl/fullUrl, and getBaseUrl when making the changes.

@ChuxiJ ChuxiJ merged commit c11a74e into ace-step:main Feb 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments