From be78c2b946b85882f7849a88a537ccb060cf14db Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:42:34 +0530 Subject: [PATCH 01/18] [MWPW-192702] Fixing issues observed from evaluating the errors in analytics dashboard. (#740) **Issue Fix Descriptions** : Explained in the comments **Testing Notes:** Refer to [wiki](https://wiki.corp.adobe.com/spaces/adobedotcom/pages/3835093951/PR+Testing+Notes+%E2%80%94+Analytics+Upload+Reliability+Fixes) Resolves: [MWPW-192702](https://jira.corp.adobe.com/browse/MWPW-192702) **Test URLs:** Before: https://main--cc--adobecom.aem.page/products/firefly/features/remove-object-from-photo?unitylibs=stage After: https://main--cc--adobecom.aem.page/products/firefly/features/remove-object-from-photo?unitylibs=analyticsIssues --------- Co-authored-by: Arushi Gupta --- .../workflow-upload/action-binder.test.js | 81 ++++-- .../workflow-upload/upload-handler.test.js | 48 ++-- .../scripts/transition-screen.test.js | 260 ++++++++++++++++++ .../workflow/workflow-upload/action-binder.js | 26 +- .../workflow-upload/upload-handler.js | 26 +- unitylibs/utils/NetworkUtils.js | 16 +- unitylibs/utils/chunkingUtils.js | 8 +- 7 files changed, 393 insertions(+), 72 deletions(-) diff --git a/test/core/workflow/workflow-upload/action-binder.test.js b/test/core/workflow/workflow-upload/action-binder.test.js index 6af3f09f..9f433687 100644 --- a/test/core/workflow/workflow-upload/action-binder.test.js +++ b/test/core/workflow/workflow-upload/action-binder.test.js @@ -218,24 +218,6 @@ describe('Unity Upload Block', () => { } }); - it('should scan image for safety', async () => { - const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - - actionBinder.serviceHandler = { postCallToService: async () => ({ status: 200 }) }; - - await actionBinder.scanImgForSafety('test-asset-id'); - }); - - it('should handle scan image for safety with retry', async () => { - fetchStub.resolves({ status: 429 }); - - const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - - actionBinder.serviceHandler = { postCallToService: async () => ({ status: 429 }) }; - - await actionBinder.scanImgForSafety('test-asset-id'); - }); - it('should upload asset', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); @@ -547,6 +529,55 @@ describe('Unity Upload Block', () => { const files = actionBinder.extractFiles(mockEvent); expect(files).to.have.length(1); }); + + it('should return empty array when dataTransfer has no file items', () => { + const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); + const mockEvent = { + dataTransfer: { items: [{ kind: 'string', getAsFile: () => null }] }, + }; + expect(actionBinder.extractFiles(mockEvent)).to.deep.equal([]); + }); + + it('should return empty array when target has no files', () => { + const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); + const mockEvent = { dataTransfer: null, target: { files: [] } }; + expect(actionBinder.extractFiles(mockEvent)).to.deep.equal([]); + }); + }); + + describe('preventDefault helper', () => { + it('should call preventDefault and stopPropagation on the event', () => { + const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); + const ev = { preventDefault: sinon.spy(), stopPropagation: sinon.spy() }; + actionBinder.preventDefault(ev); + expect(ev.preventDefault.calledOnce).to.be.true; + expect(ev.stopPropagation.calledOnce).to.be.true; + }); + }); + + describe('getAdditionalHeaders', () => { + it('should append verb to x-unity-action when verb is set', () => { + const wf = { + ...workflowCfg, + supportedFeatures: { values: () => ({ next: () => ({ value: 'remove-object' }) }) }, + }; + const actionBinder = new ActionBinder(unityEl, wf, unityEl, [unityEl]); + actionBinder.verb = 'v2'; + const headers = actionBinder.getAdditionalHeaders(); + expect(headers['x-unity-action']).to.equal('remove-object-v2'); + expect(headers['x-unity-product']).to.equal(wf.productName); + }); + + it('should use base action only when verb is unset', () => { + const wf = { + ...workflowCfg, + supportedFeatures: { values: () => ({ next: () => ({ value: 'upload-only' }) }) }, + }; + const actionBinder = new ActionBinder(unityEl, wf, unityEl, [unityEl]); + actionBinder.verb = undefined; + const headers = actionBinder.getAdditionalHeaders(); + expect(headers['x-unity-action']).to.equal('upload-only'); + }); }); describe('Action Maps and Event Handling', () => { @@ -624,6 +655,20 @@ describe('Unity Upload Block', () => { await actionBinder.executeActionMaps('unknown', []); }); + it('should log analytics for redirect action', async () => { + const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); + actionBinder.transitionScreen = { splashScreenEl: document.createElement('div') }; + actionBinder.errorToastEl = document.createElement('div'); + sinon.stub(actionBinder, 'loadTransitionScreen').resolves(); + sinon.stub(actionBinder, 'handlePreloads').resolves(); + const spy = sinon.stub(actionBinder, 'logAnalyticsinSplunk'); + await actionBinder.executeActionMaps('redirect'); + expect(spy.calledWith('Edit Photos CTA|UnityWidget', {})).to.be.true; + spy.restore(); + actionBinder.loadTransitionScreen.restore(); + actionBinder.handlePreloads.restore(); + }); + it('should initialize action listeners', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); diff --git a/test/core/workflow/workflow-upload/upload-handler.test.js b/test/core/workflow/workflow-upload/upload-handler.test.js index f3496efd..e998e7bc 100644 --- a/test/core/workflow/workflow-upload/upload-handler.test.js +++ b/test/core/workflow/workflow-upload/upload-handler.test.js @@ -209,12 +209,13 @@ describe('UploadHandler', () => { }); it('should handle abort signal', async () => { - const signal = { aborted: true }; + const controller = new AbortController(); + controller.abort(); + const signal = controller.signal; const file = new File(['test data'], 'test.txt', { type: 'text/plain' }); const uploadUrls = ['http://upload1.com']; const blockSize = 20; - // Set up fetch stub window.fetch = sinon.stub(); const result = await uploadHandler.uploadChunksToUnity(uploadUrls, file, blockSize, signal); @@ -267,19 +268,19 @@ describe('UploadHandler', () => { }); describe('uploadFileToUnity Error Handling', () => { - let originalFetch; + let originalFetchFromServiceWithRetry; beforeEach(() => { - originalFetch = window.fetch; + originalFetchFromServiceWithRetry = uploadHandler.networkUtils.fetchFromServiceWithRetry; }); afterEach(() => { - window.fetch = originalFetch; + uploadHandler.networkUtils.fetchFromServiceWithRetry = originalFetchFromServiceWithRetry; }); it('should handle upload failure with no statusText', async () => { - const mockResponse = { ok: false, status: 500 }; - window.fetch = sinon.stub().resolves(mockResponse); + const retryError = new Error('Upload request failed, Max retry delay exceeded for URL: http://upload.com'); + uploadHandler.networkUtils.fetchFromServiceWithRetry = sinon.stub().rejects(retryError); const blob = new Blob(['test data'], { type: 'text/plain' }); @@ -294,7 +295,7 @@ describe('UploadHandler', () => { it('should handle AbortError during upload', async () => { const abortError = new Error('Request aborted'); abortError.name = 'AbortError'; - window.fetch = sinon.stub().rejects(abortError); + uploadHandler.networkUtils.fetchFromServiceWithRetry = sinon.stub().rejects(abortError); const blob = new Blob(['test data'], { type: 'text/plain' }); @@ -302,14 +303,14 @@ describe('UploadHandler', () => { await uploadHandler.uploadFileToUnity('http://upload.com', blob, 'text/plain', 'asset-123'); expect.fail('Should have thrown AbortError'); } catch (error) { - expect(error.message).to.include('Max retry delay exceeded'); + expect(error.name).to.equal('AbortError'); } }); it('should handle Timeout error during upload', async () => { - const timeoutError = new Error('Request timed out'); - timeoutError.name = 'Timeout'; - window.fetch = sinon.stub().rejects(timeoutError); + const timeoutError = new Error('Request timed out after 60000ms, Max retry delay exceeded for URL: http://upload.com'); + timeoutError.name = 'TimeoutError'; + uploadHandler.networkUtils.fetchFromServiceWithRetry = sinon.stub().rejects(timeoutError); const blob = new Blob(['test data'], { type: 'text/plain' }); @@ -317,29 +318,40 @@ describe('UploadHandler', () => { await uploadHandler.uploadFileToUnity('http://upload.com', blob, 'text/plain', 'asset-123'); expect.fail('Should have thrown error'); } catch (error) { - expect(error.message).to.include('Max retry delay exceeded'); + expect(error.message).to.include('Request timed out'); } }); }); describe('uploadChunksToUnity Error Handling', () => { - let originalFetch; + let originalUploadFileToUnity; beforeEach(() => { - originalFetch = window.fetch; + originalUploadFileToUnity = uploadHandler.uploadFileToUnity; }); afterEach(() => { - window.fetch = originalFetch; + uploadHandler.uploadFileToUnity = originalUploadFileToUnity; }); it('should log chunk errors when upload fails', async () => { - const mockError = new Error('Network error'); - window.fetch = sinon.stub().rejects(mockError); + const networkError = new Error('Network error'); + networkError.status = 0; + + uploadHandler.uploadFileToUnity = async () => { + mockActionBinder.logAnalyticsinSplunk('Upload Chunk Error|UnityWidget', { + errorData: { code: 'upload-error-chunk-upload', subCode: 0, desc: 'Network error' }, + assetId: mockActionBinder.assetId, + }); + throw networkError; + }; + const file = new File(['test data'], 'test.txt', { type: 'text/plain' }); const uploadUrls = ['http://upload1.com']; const blockSize = 10; + const result = await uploadHandler.uploadChunksToUnity(uploadUrls, file, blockSize); + expect(result.failedChunks.size).to.equal(1); expect(mockActionBinder.logAnalyticsinSplunk.calledWith('Upload Chunk Error|UnityWidget')).to.be.true; expect(mockActionBinder.logAnalyticsinSplunk.calledWith('Chunked Upload Failed|UnityWidget')).to.be.true; diff --git a/test/unitylibs/scripts/transition-screen.test.js b/test/unitylibs/scripts/transition-screen.test.js index 472c3582..397ec490 100644 --- a/test/unitylibs/scripts/transition-screen.test.js +++ b/test/unitylibs/scripts/transition-screen.test.js @@ -33,6 +33,36 @@ describe('TransitionScreen', () => { expect(fill.style.width).to.equal('42%'); expect(status.textContent).to.equal('42%'); }); + + it('should no-op when layer is null', () => { + expect(() => screen.updateProgressBar(null, 50)).to.not.throw(); + }); + + it('should restore progressText from lastProgressText when empty', () => { + TransitionScreen.lastProgressText = 'Uploading %'; + screen.progressText = ''; + splashScreenEl.innerHTML = ` +
+
0%
+
+
+ `; + screen.updateProgressBar(splashScreenEl, 10); + const status = splashScreenEl.querySelector('#progress-status'); + expect(status.textContent).to.equal('Uploading 10%'); + }); + + it('should cap percentage at LOADER_LIMIT', () => { + screen.LOADER_LIMIT = 80; + splashScreenEl.innerHTML = ` +
+
0%
+
+
+ `; + screen.updateProgressBar(splashScreenEl, 99); + expect(splashScreenEl.querySelector('.spectrum-ProgressBar').getAttribute('value')).to.equal('80'); + }); }); describe('createProgressBar', () => { @@ -61,6 +91,52 @@ describe('TransitionScreen', () => { spy.restore(); clock.restore(); }); + + it('should return early when splash element is missing', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(screen, 'updateProgressBar'); + screen.progressBarHandler(null, 10, 10, true); + clock.tick(100); + expect(spy.called).to.be.false; + spy.restore(); + clock.restore(); + }); + + it('should return early when current value already at LOADER_LIMIT', () => { + screen.LOADER_LIMIT = 70; + splashScreenEl.innerHTML = ` +
+
70%
+
+
+ `; + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(screen, 'updateProgressBar'); + screen.progressBarHandler(splashScreenEl, 10, 10, false); + clock.tick(100); + expect(spy.called).to.be.false; + spy.restore(); + clock.restore(); + }); + + it('should return early inside timeout when value is 100', () => { + splashScreenEl.innerHTML = ` +
+
0%
+
+
+ `; + const clock = sinon.useFakeTimers(); + const stub = sinon.stub(screen, 'updateProgressBar').callsFake((layer, pct) => { + if (pct === 10) { + layer.querySelector('.spectrum-ProgressBar').setAttribute('value', '100'); + } + }); + screen.progressBarHandler(splashScreenEl, 10, 10, true); + clock.tick(20); + stub.restore(); + clock.restore(); + }); }); describe('handleSplashProgressBar', () => { @@ -110,6 +186,20 @@ describe('TransitionScreen', () => { expect(document.querySelector('main').getAttribute('aria-hidden')).to.equal('true'); stub.restore(); }); + + it('should focus splash element after short delay when shown', () => { + const stubPb = sinon.stub(screen, 'progressBarHandler'); + sinon.stub(screen, 'resetSplashVideos'); + splashScreenEl.setAttribute('tabindex', '-1'); + splashScreenEl.focus = sinon.spy(); + const clock = sinon.useFakeTimers(); + screen.splashVisibilityController(true); + clock.tick(50); + expect(splashScreenEl.focus.calledOnce).to.be.true; + stubPb.restore(); + screen.resetSplashVideos.restore(); + clock.restore(); + }); }); describe('updateCopyForDevice', () => { @@ -215,5 +305,175 @@ describe('TransitionScreen', () => { }; expect(screen.getFragmentLink(undefined)).to.equal(fragmentLink); }); + + it('should return themed fragment link for workflow-upload when theme is set', () => { + screen.workflowCfg = { + name: 'workflow-upload', + theme: 'dark', + productName: 'Firefly', + targetCfg: { + splashScreenConfig: { + fragmentLink: '/default', + 'fragmentLink-firefly': '/ff-light', + 'fragmentLink-firefly-dark': '/ff-dark', + }, + }, + }; + expect(screen.getFragmentLink(undefined)).to.equal('/ff-dark'); + }); + + it('should fall back to product fragment when theme set but no themed key exists', () => { + screen.workflowCfg = { + name: 'workflow-upload', + theme: 'dark', + productName: 'Firefly', + targetCfg: { + splashScreenConfig: { + fragmentLink: '/default', + 'fragmentLink-firefly': '/ff-only', + }, + }, + }; + expect(screen.getFragmentLink(undefined)).to.equal('/ff-only'); + }); + }); + + describe('checkForProgressBar', () => { + it('should return icon-progress-bar element when present', () => { + const icon = document.createElement('div'); + icon.className = 'icon-progress-bar'; + splashScreenEl.appendChild(icon); + expect(screen.checkForProgressBar()).to.equal(icon); + }); + + it('should build progress bar from [[progress-bar]] placeholder paragraph', () => { + const p = document.createElement('p'); + p.textContent = 'Status [[progress-bar]] more text'; + splashScreenEl.appendChild(p); + const result = screen.checkForProgressBar(); + expect(result.classList.contains('progress-bar-area')).to.be.true; + expect(result.querySelector('.progress-bar')).to.exist; + expect(p.textContent).to.not.include('[[progress-bar]]'); + }); + + it('should return null when no progress UI markers exist', () => { + splashScreenEl.innerHTML = '

Plain copy only

'; + expect(screen.checkForProgressBar()).to.be.null; + }); + }); + + describe('setProgressTextFromDOM', () => { + it('should collect text nodes next to progress-bar span', () => { + splashScreenEl.innerHTML = ` +
+ + Uploading your file +
`; + const nodes = screen.setProgressTextFromDOM(); + expect(nodes.length).to.be.at.least(1); + expect(screen.progressText).to.include('Uploading'); + }); + }); + + describe('resetSplashVideos', () => { + it('should return when splashScreenEl is missing', () => { + screen.splashScreenEl = null; + expect(() => screen.resetSplashVideos()).to.not.throw(); + }); + + it('should reset and play video when readyState is high enough', () => { + const video = document.createElement('video'); + Object.defineProperty(video, 'readyState', { value: 4, configurable: true }); + sinon.stub(video, 'play').resolves(); + sinon.stub(video, 'load'); + splashScreenEl.appendChild(video); + screen.resetSplashVideos(); + expect(video.load.called).to.be.true; + video.play.restore(); + video.load.restore(); + }); + + it('should wait for canplay when video is not ready', () => { + const video = document.createElement('video'); + Object.defineProperty(video, 'readyState', { value: 0, configurable: true }); + sinon.stub(video, 'load'); + sinon.stub(video, 'play').resolves(); + splashScreenEl.appendChild(video); + screen.resetSplashVideos(); + video.dispatchEvent(new Event('canplay')); + expect(video.load.called).to.be.true; + video.play.restore(); + video.load.restore(); + }); + }); + + describe('loadSplashFragment', () => { + it('should return immediately when showSplashScreen is false', async () => { + screen.workflowCfg = { + targetCfg: { showSplashScreen: false, splashScreenConfig: { splashScreenParent: 'body' } }, + productName: 'test', + name: 'workflow-upload', + }; + const fetchSpy = sinon.spy(window, 'fetch'); + await screen.loadSplashFragment(); + expect(fetchSpy.called).to.be.false; + fetchSpy.restore(); + }); + }); + + describe('delayedSplashLoader', () => { + it('should call loadSplashFragment after idle timeout', async () => { + const stub = sinon.stub(screen, 'loadSplashFragment').resolves(); + const clock = sinon.useFakeTimers(); + screen.delayedSplashLoader(); + clock.tick(8000); + await Promise.resolve(); + await Promise.resolve(); + expect(stub.calledOnce).to.be.true; + stub.restore(); + clock.restore(); + }); + + it('should call loadSplashFragment on first pointer interaction', async () => { + const stub = sinon.stub(screen, 'loadSplashFragment').resolves(); + screen.delayedSplashLoader(); + document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true })); + await Promise.resolve(); + await Promise.resolve(); + expect(stub.calledOnce).to.be.true; + stub.restore(); + }); + }); + + describe('showSplashScreen photoshop branch', () => { + it('should call updateCopyForDevice when product is Photoshop', async () => { + splashScreenEl.classList.add('decorate'); + const icon = document.createElement('div'); + icon.className = 'icon-progress-bar'; + splashScreenEl.appendChild(icon); + const h0 = document.createElement('h1'); + h0.innerText = 'H0'; + const h1 = document.createElement('h2'); + h1.innerText = 'H1'; + const h2 = document.createElement('h3'); + h2.innerText = 'H2'; + const h3 = document.createElement('h4'); + h3.innerText = 'H3'; + splashScreenEl.appendChild(h0); + splashScreenEl.appendChild(h1); + splashScreenEl.appendChild(h2); + splashScreenEl.appendChild(h3); + screen.workflowCfg = { + targetCfg: { showSplashScreen: true }, + productName: 'Photoshop', + name: 'workflow-upload', + }; + const stub = sinon.stub(screen, 'updateCopyForDevice'); + const stubPb = sinon.stub(screen, 'handleSplashProgressBar').resolves(); + await screen.showSplashScreen(); + expect(stub.called).to.be.true; + stub.restore(); + stubPb.restore(); + }); }); }); diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index d74726f9..517bf8cb 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -186,30 +186,6 @@ export default class ActionBinder { } } - async scanImgForSafety(assetId, signal) { - if (signal?.aborted) { - const err = new Error('Operation aborted'); - err.name = 'AbortError'; - throw err; - } - const assetData = { assetId, targetProduct: this.workflowCfg.productName }; - const optionsBody = { body: JSON.stringify(assetData), ...(signal && { signal }) }; - const res = await this.serviceHandler.postCallToService( - this.apiConfig.endPoint.acmpCheck, - optionsBody, - {}, - false, - ); - if (res.status === 429 || (res.status >= 500 && res.status < 600)) { - if (signal?.aborted) { - const err = new Error('Operation aborted'); - err.name = 'AbortError'; - throw err; - } - setTimeout(() => { this.scanImgForSafety(assetId, signal); }, 1000); - } - } - async uploadAsset(file) { const assetDetails = { targetProduct: this.workflowCfg.productName, @@ -257,7 +233,7 @@ export default class ActionBinder { ); } else { await this.uploadImgToUnity(href, id, file, file.type, signal); - await this.scanImgForSafety(this.assetId, signal); + await uploadHandler.scanImgForSafetyWithRetry(this.assetId, signal); this.logAnalyticsinSplunk('Upload Completed|UnityWidget', { assetId: this.assetId }); } return true; diff --git a/unitylibs/core/workflow/workflow-upload/upload-handler.js b/unitylibs/core/workflow/workflow-upload/upload-handler.js index 57d5bf29..f14ed0fa 100644 --- a/unitylibs/core/workflow/workflow-upload/upload-handler.js +++ b/unitylibs/core/workflow/workflow-upload/upload-handler.js @@ -46,24 +46,37 @@ export default class UploadHandler { error.status = response.status; throw error; }; - const onError = (error) => { + const onError = (error, attempt) => { if (error.name !== 'AbortError') { - this.logError('Upload Chunk Error|UnityWidget', { + const isFinalAttempt = attempt >= retryConfig.retryParams.maxRetries; + const eventName = isFinalAttempt ? 'Upload Chunk Error|UnityWidget' : 'Upload Chunk Warn|UnityWidget'; + const code = isFinalAttempt ? 'upload-error-chunk-upload' : 'upload-warn-chunk-upload'; + this.logError(eventName, { chunkNumber, size: blobData.size, fileType, errorData: { - code: 'upload-chunk-error', - desc: `Exception during chunk ${chunkNumber} upload: ${error.message}`, + code, + subCode: error.status ?? (error.name === 'TimeoutError' ? 504 : 0), + desc: `Exception during chunk ${chunkNumber} upload (attempt ${attempt}): ${error.message}`, }, }, `Message: Exception raised when uploading chunk to Unity, Error: ${error.message}, Asset ID: ${assetId}, ${blobData.size} bytes`); + if (isFinalAttempt) { + this.chunkAbortController?.abort(); + throw error; + } + } else { + throw error; } - throw error; }; return this.networkUtils.fetchFromServiceWithRetry(storageUrl, uploadOptions, retryConfig, onSuccess, onError); } async uploadChunksToUnity(uploadUrls, file, blockSize, signal = null) { + this.chunkAbortController = new AbortController(); + const mergedSignal = signal + ? AbortSignal.any([signal, this.chunkAbortController.signal]) + : this.chunkAbortController.signal; const options = { assetId: this.actionBinder.assetId, fileType: file.type, @@ -73,9 +86,10 @@ export default class UploadHandler { file, blockSize, this.uploadFileToUnity.bind(this), - signal, + mergedSignal, options, ); + this.chunkAbortController = null; const { failedChunks, attemptMap } = result; const totalChunks = Math.ceil(file.size / blockSize); if (failedChunks.size > 0 && !signal?.aborted) { diff --git a/unitylibs/utils/NetworkUtils.js b/unitylibs/utils/NetworkUtils.js index 555628b9..82432979 100644 --- a/unitylibs/utils/NetworkUtils.js +++ b/unitylibs/utils/NetworkUtils.js @@ -10,8 +10,10 @@ export default class NetworkUtils { async fetchWithTimeout(url, options = {}, timeoutMs = 60000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); - const passedSignal = options.signal || controller.signal; - const mergedOptions = { ...options, signal: passedSignal }; + const mergedSignal = options.signal + ? AbortSignal.any([controller.signal, options.signal]) + : controller.signal; + const mergedOptions = { ...options, signal: mergedSignal }; try { const response = await fetch(url, mergedOptions); clearTimeout(timeout); @@ -19,6 +21,7 @@ export default class NetworkUtils { } catch (e) { clearTimeout(timeout); if (e.name === 'AbortError') { + if (options.signal?.aborted) throw e; const error = new Error(`Request timed out after ${timeoutMs}ms`); error.name = 'TimeoutError'; throw error; @@ -101,6 +104,15 @@ export default class NetworkUtils { const onSuccessWithAttempt = onSuccess ? (response) => onSuccess(response, attempt) : null; const onErrorWithAttempt = onError ? (error) => onError(error, attempt) : null; let response = await this.fetchFromService(url, options, onSuccessWithAttempt, onErrorWithAttempt); + if (!response) { + if (attempt < maxRetries) { + const delay = retryDelay; + await new Promise((resolve) => { setTimeout(resolve, delay); }); + retryDelay *= 2; + continue; + } + break; + } const customRetryCheckResult = retryConfig.extraRetryCheck && await retryConfig.extraRetryCheck(response); if ((customRetryCheckResult || response.status === 202 || (response.status >= 500 && response.status < 600) || response.status === 429)) { if (attempt < maxRetries) { diff --git a/unitylibs/utils/chunkingUtils.js b/unitylibs/utils/chunkingUtils.js index 21bb554a..91fc1bcf 100644 --- a/unitylibs/utils/chunkingUtils.js +++ b/unitylibs/utils/chunkingUtils.js @@ -48,9 +48,11 @@ export async function createChunkUploadTasks(uploadUrls, file, blockSize, upload if (onChunkComplete) onChunkComplete(i, chunkNumber, result); return result; } catch (err) { - const chunkInfo = { chunkIndex: i, chunkNumber }; - failedChunks.add(chunkInfo); - if (onChunkError) onChunkError(chunkInfo, err); + if (err.name !== 'AbortError') { + const chunkInfo = { chunkIndex: i, chunkNumber }; + failedChunks.add(chunkInfo); + if (onChunkError) onChunkError(chunkInfo, err); + } throw err; } })(); From 14d6e4fbdac0fb93630ce1fd9aa3857155b291a7 Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:54:39 +0530 Subject: [PATCH 02/18] [MWPW-189488, MWPW-189974] : Bug fixes (#742) Resolves: [MWPW-189974](https://jira.corp.adobe.com/browse/MWPW-189974) [MWPW-189488](https://jira.corp.adobe.com/browse/MWPW-189488) **Test URLs:** - Before: https://main--cc--adobecom.aem.page/products/firefly/features/remove-object-from-photo?unitylibs=stage - After: https://main--cc--adobecom.aem.page/products/firefly/features/remove-object-from-photo?unitylibs=bugfixesv2 --------- Co-authored-by: Arushi Gupta --- .../workflow/workflow-upload/action-binder.js | 22 +++++++++++++++++-- unitylibs/scripts/transition-screen.js | 13 ++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index 517bf8cb..9a773948 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -345,7 +345,11 @@ export default class ActionBinder { } const finalResults = await Promise.allSettled(this.promiseStack); if (finalResults.some((result) => result.status === 'rejected')) return; - window.location.href = response.url; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + window.location.href = response.url; + }); + }); } catch (e) { if (e.message === 'Operation termination requested.') return; await this.transitionScreen.showSplashScreen(); @@ -495,7 +499,20 @@ export default class ActionBinder { }); }, INPUT: (el, key) => { - el.addEventListener('click', () => { + let isFilePickerOpen = false; + el.addEventListener('click', (e) => { + if (isFilePickerOpen) { + e.preventDefault(); + return; + } + isFilePickerOpen = true; + const releaseFilePickerLock = () => { isFilePickerOpen = false; }; + window.addEventListener('focus', releaseFilePickerLock, { once: true }); + document.addEventListener('visibilitychange', function onPageVisible() { + if (document.visibilityState !== 'visible') return; + releaseFilePickerLock(); + document.removeEventListener('visibilitychange', onPageVisible); + }); this.canvasArea.forEach((element) => { const errHolder = element.querySelector('.alert-holder'); if (errHolder?.classList.contains('show')) { @@ -505,6 +522,7 @@ export default class ActionBinder { }); }); el.addEventListener('change', async (e) => { + isFilePickerOpen = false; const files = this.extractFiles(e); this.filesData = { count: files.length, size: files[0].size, type: files[0].type }; this.logAnalyticsinSplunk('Click Drag and drop|UnityWidget', { assetId: this.assetId, fileMetaData: this.filesData }); diff --git a/unitylibs/scripts/transition-screen.js b/unitylibs/scripts/transition-screen.js index 800b1642..f0d038df 100644 --- a/unitylibs/scripts/transition-screen.js +++ b/unitylibs/scripts/transition-screen.js @@ -20,6 +20,14 @@ export default class TransitionScreen { this.isDesktop = isDesktop; this.headingElements = []; this.progressText = ''; + this._progressBarTimeout = null; + } + + cancelProgressBar() { + if (this._progressBarTimeout !== null) { + clearTimeout(this._progressBarTimeout); + this._progressBarTimeout = null; + } } setProgressTextFromDOM() { @@ -69,7 +77,8 @@ export default class TransitionScreen { if (currentValue === 100 || currentValue >= this.LOADER_LIMIT) return; } - setTimeout(() => { + this._progressBarTimeout = setTimeout(() => { + this._progressBarTimeout = null; const v = initialize ? 0 : parseInt(progressBar.getAttribute('value'), 10); if (v === 100) return; this.updateProgressBar(s, v + newI); @@ -207,6 +216,7 @@ export default class TransitionScreen { splashVisibilityController(displayOn) { if (!displayOn) { + this.cancelProgressBar(); this.LOADER_LIMIT = 95; this.splashScreenEl.parentElement?.classList.remove('hide-splash-overflow'); this.splashScreenEl.classList.remove('show'); @@ -215,6 +225,7 @@ export default class TransitionScreen { document.querySelector('footer').removeAttribute('aria-hidden'); return; } + this.cancelProgressBar(); this.progressBarHandler(this.splashScreenEl, this.LOADER_DELAY, this.LOADER_INCREMENT, true); this.resetSplashVideos(); this.splashScreenEl.classList.add('show'); From dc69e3a5759f5e03ea22ca85dabd119bd49e3113 Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:25:50 -0700 Subject: [PATCH 03/18] MWPW-191927 Implementing Config-Based A/B Testing (#747) * Implementing fetch request to page config API post-LCP * Scalable and backwards-compatible approach to fetch experiment data and leverage location header for successive API calls Resolves: [MWPW-191927](https://jira.corp.adobe.com/browse/MWPW-191927) **Test URLs:** - Before: https://main--dc--adobecom.aem.page/acrobat/online/compress-pdf?unitylibs=stage - After: https://main--dc--adobecom.aem.page/acrobat/online/compress-pdf?unitylibs=MWPW-191927 --- test/utils/experiment-provider.test.js | 2 +- .../workflow-acrobat/action-binder.js | 56 +++++++++++++------ unitylibs/scripts/utils.js | 13 +++++ unitylibs/utils/experiment-provider.js | 14 ++--- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 3a397cac..25f1378f 100644 --- a/test/utils/experiment-provider.test.js +++ b/test/utils/experiment-provider.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { expect } from '@esm-bundle/chai'; -import { getExperimentData, getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; +import getExperimentData, { getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; describe('getExperimentData', () => { // Helper function to setup mock with result and error diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 7af73e30..271aaf41 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -161,7 +161,7 @@ export default class ActionBinder { this.actionMap = actionMap; this.limits = {}; this.operations = []; - this.acrobatApiConfig = this.getAcrobatApiConfig(); + this.acrobatApiConfig = null; this.networkUtils = new NetworkUtils(); this.uploadHandler = null; this.splashScreenEl = null; @@ -182,6 +182,9 @@ export default class ActionBinder { this.multiFileValidationFailure = false; this.initialize(); this.experimentData = null; + this.experimentViaPageConfig = false; + this.pageConfigLocation = null; + this.pageConfigFetched = false; } async initialize() { @@ -224,11 +227,13 @@ export default class ActionBinder { } getAcrobatApiConfig() { + const base = this.pageConfigLocation || unityConfig.apiEndPoint; unityConfig.acrobatEndpoint = { - createAsset: `${unityConfig.apiEndPoint}/asset`, - finalizeAsset: `${unityConfig.apiEndPoint}/asset/finalize`, - getMetadata: `${unityConfig.apiEndPoint}/asset/metadata`, + createAsset: `${base}/asset`, + finalizeAsset: `${base}/asset/finalize`, + getMetadata: `${base}/asset/metadata`, }; + unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; } @@ -242,18 +247,6 @@ export default class ActionBinder { } async handlePreloads() { - if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0])) { - const { getExperimentData, getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); - try { - const decisionScopes = await getDecisionScopesForVerb(this.workflowCfg.enabledFeatures[0]); - this.experimentData = await getExperimentData(decisionScopes); - } catch (error) { - await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { - code: 'warn_fetch_experiment', - desc: error.message, - }); - } - } const parr = []; if (this.workflowCfg.targetCfg.showSplashScreen) { parr.push( @@ -263,6 +256,32 @@ export default class ActionBinder { await priorityLoad(parr); } + async ensurePageConfig() { + if (this.pageConfigFetched) return; + this.pageConfigFetched = true; + const verb = this.workflowCfg.enabledFeatures[0]; + try { + const { fetchPageConfig } = await import('../../../scripts/utils.js'); + const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); + const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); + this.pageConfigLocation = pageConfig.location; + if (pageConfig.config?.target?.enabled) { + this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); + this.experimentViaPageConfig = true; + } else if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(verb)) { + const { getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); + const decisionScopes = await getDecisionScopesForVerb(verb); + this.experimentData = await getExperimentData(decisionScopes); + } + } catch (error) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + } + this.acrobatApiConfig = this.getAcrobatApiConfig(); + } + async dispatchErrorToast(errorType, status, info = null, lanaOnly = false, showError = true, errorMetaData = {}) { if (!showError) return; const errorMessage = errorType in this.workflowCfg.errors @@ -457,7 +476,7 @@ export default class ActionBinder { redirectUrl = url.href; } } - this.redirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; }) .catch(async (e) => { await this.showTransitionScreen(); @@ -486,7 +505,7 @@ export default class ActionBinder { if (this.multiFileValidationFailure) cOpts.payload.feedback = 'uploaderror'; if (this.showInfoToast) cOpts.payload.feedback = 'nonpdf'; } - if (this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]) && this.experimentData) { + if (this.experimentData && (this.experimentViaPageConfig || this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]))) { cOpts.payload.variationId = this.experimentData.variationId; } await this.getRedirectUrl(cOpts); @@ -556,6 +575,7 @@ export default class ActionBinder { if (prevalidatedFiles.length === 0) return; const { isValid, validFiles } = await this.validateFiles(prevalidatedFiles); if (!isValid) return; + await this.ensurePageConfig(); await this.initUploadHandler(); if (files.length === 1 || (validFiles.length === 1 && !verbsWithoutFallback.includes(this.workflowCfg.enabledFeatures[0]))) { await this.handleSingleFileUpload(validFiles); diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index b8fc7c57..ce6cbc44 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -297,12 +297,14 @@ export const unityConfig = (() => { prod: { apiEndPoint: 'https://unity.adobe.io/api/v1', connectorApiEndPoint: 'https://unity.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.adobe.com/api/v1/pageConfig', env: 'prod', ...commoncfg, }, stage: { apiEndPoint: 'https://unity-stage.adobe.io/api/v1', connectorApiEndPoint: 'https://unity-stage.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.stage.adobe.com/api/v1/pageConfig', env: 'stage', ...commoncfg, }, @@ -335,3 +337,14 @@ export function sendAnalyticsEvent(event) { export function getMatchedDomain(domainMap = {}, hostname = window.location.hostname) { return Object.keys(domainMap).find((domain) => domainMap[domain].some((pattern) => new RegExp(pattern).test(hostname))); } + +export async function fetchPageConfig({ product, verb }) { + try { + const url = `${unityConfig.pageConfigEndPoint}?product=${product}&verb=${verb}`; + const resp = await fetch(url, { headers: { 'x-api-key': unityConfig.apiKey } }); + if (!resp.ok) throw new Error(`PageConfig fetch failed: ${resp.statusText}`); + return resp.json(); + } catch (e) { + return {}; + } +} diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index 013755c2..f102ba72 100644 --- a/unitylibs/utils/experiment-provider.js +++ b/unitylibs/utils/experiment-provider.js @@ -1,11 +1,5 @@ /* eslint-disable no-underscore-dangle */ -export async function getDecisionScopesForVerb(verb) { - const region = await getRegion().catch(() => undefined); - const verbScope = `acom_unity_acrobat_${verb}`; - return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; -} - export async function getRegion() { const resp = await fetch('https://geo2.adobe.com/json/', { cache: 'no-cache' }); if (!resp.ok) throw new Error(`Failed to resolve region: ${resp.statusText}`); @@ -14,7 +8,13 @@ export async function getRegion() { return country.toLowerCase(); } -export async function getExperimentData(decisionScopes) { +export async function getDecisionScopesForVerb(verb) { + const region = await getRegion().catch(() => undefined); + const verbScope = `acom_unity_acrobat_${verb}`; + return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; +} + +export default async function getExperimentData(decisionScopes) { if (!decisionScopes || decisionScopes.length === 0) { throw new Error('No decision scopes provided for experiment data fetch'); } From 6ce58c3938dc3a3b3785afce5014adbdf319686b Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:40:11 -0700 Subject: [PATCH 04/18] Revert "MWPW-191927 Implementing Config-Based A/B Testing" (#755) Reverts adobecom/unity#747 --- test/utils/experiment-provider.test.js | 2 +- .../workflow-acrobat/action-binder.js | 56 ++++++------------- unitylibs/scripts/utils.js | 13 ----- unitylibs/utils/experiment-provider.js | 14 ++--- 4 files changed, 26 insertions(+), 59 deletions(-) diff --git a/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 25f1378f..3a397cac 100644 --- a/test/utils/experiment-provider.test.js +++ b/test/utils/experiment-provider.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { expect } from '@esm-bundle/chai'; -import getExperimentData, { getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; +import { getExperimentData, getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; describe('getExperimentData', () => { // Helper function to setup mock with result and error diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 271aaf41..7af73e30 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -161,7 +161,7 @@ export default class ActionBinder { this.actionMap = actionMap; this.limits = {}; this.operations = []; - this.acrobatApiConfig = null; + this.acrobatApiConfig = this.getAcrobatApiConfig(); this.networkUtils = new NetworkUtils(); this.uploadHandler = null; this.splashScreenEl = null; @@ -182,9 +182,6 @@ export default class ActionBinder { this.multiFileValidationFailure = false; this.initialize(); this.experimentData = null; - this.experimentViaPageConfig = false; - this.pageConfigLocation = null; - this.pageConfigFetched = false; } async initialize() { @@ -227,13 +224,11 @@ export default class ActionBinder { } getAcrobatApiConfig() { - const base = this.pageConfigLocation || unityConfig.apiEndPoint; unityConfig.acrobatEndpoint = { - createAsset: `${base}/asset`, - finalizeAsset: `${base}/asset/finalize`, - getMetadata: `${base}/asset/metadata`, + createAsset: `${unityConfig.apiEndPoint}/asset`, + finalizeAsset: `${unityConfig.apiEndPoint}/asset/finalize`, + getMetadata: `${unityConfig.apiEndPoint}/asset/metadata`, }; - unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; } @@ -247,6 +242,18 @@ export default class ActionBinder { } async handlePreloads() { + if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0])) { + const { getExperimentData, getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); + try { + const decisionScopes = await getDecisionScopesForVerb(this.workflowCfg.enabledFeatures[0]); + this.experimentData = await getExperimentData(decisionScopes); + } catch (error) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + } + } const parr = []; if (this.workflowCfg.targetCfg.showSplashScreen) { parr.push( @@ -256,32 +263,6 @@ export default class ActionBinder { await priorityLoad(parr); } - async ensurePageConfig() { - if (this.pageConfigFetched) return; - this.pageConfigFetched = true; - const verb = this.workflowCfg.enabledFeatures[0]; - try { - const { fetchPageConfig } = await import('../../../scripts/utils.js'); - const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); - const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); - this.pageConfigLocation = pageConfig.location; - if (pageConfig.config?.target?.enabled) { - this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); - this.experimentViaPageConfig = true; - } else if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(verb)) { - const { getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); - const decisionScopes = await getDecisionScopesForVerb(verb); - this.experimentData = await getExperimentData(decisionScopes); - } - } catch (error) { - await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { - code: 'warn_fetch_experiment', - desc: error.message, - }); - } - this.acrobatApiConfig = this.getAcrobatApiConfig(); - } - async dispatchErrorToast(errorType, status, info = null, lanaOnly = false, showError = true, errorMetaData = {}) { if (!showError) return; const errorMessage = errorType in this.workflowCfg.errors @@ -476,7 +457,7 @@ export default class ActionBinder { redirectUrl = url.href; } } - this.redirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; }) .catch(async (e) => { await this.showTransitionScreen(); @@ -505,7 +486,7 @@ export default class ActionBinder { if (this.multiFileValidationFailure) cOpts.payload.feedback = 'uploaderror'; if (this.showInfoToast) cOpts.payload.feedback = 'nonpdf'; } - if (this.experimentData && (this.experimentViaPageConfig || this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]))) { + if (this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]) && this.experimentData) { cOpts.payload.variationId = this.experimentData.variationId; } await this.getRedirectUrl(cOpts); @@ -575,7 +556,6 @@ export default class ActionBinder { if (prevalidatedFiles.length === 0) return; const { isValid, validFiles } = await this.validateFiles(prevalidatedFiles); if (!isValid) return; - await this.ensurePageConfig(); await this.initUploadHandler(); if (files.length === 1 || (validFiles.length === 1 && !verbsWithoutFallback.includes(this.workflowCfg.enabledFeatures[0]))) { await this.handleSingleFileUpload(validFiles); diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index ce6cbc44..b8fc7c57 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -297,14 +297,12 @@ export const unityConfig = (() => { prod: { apiEndPoint: 'https://unity.adobe.io/api/v1', connectorApiEndPoint: 'https://unity.adobe.io/api/v1/asset/connector', - pageConfigEndPoint: 'https://cdn-unity.adobe.com/api/v1/pageConfig', env: 'prod', ...commoncfg, }, stage: { apiEndPoint: 'https://unity-stage.adobe.io/api/v1', connectorApiEndPoint: 'https://unity-stage.adobe.io/api/v1/asset/connector', - pageConfigEndPoint: 'https://cdn-unity.stage.adobe.com/api/v1/pageConfig', env: 'stage', ...commoncfg, }, @@ -337,14 +335,3 @@ export function sendAnalyticsEvent(event) { export function getMatchedDomain(domainMap = {}, hostname = window.location.hostname) { return Object.keys(domainMap).find((domain) => domainMap[domain].some((pattern) => new RegExp(pattern).test(hostname))); } - -export async function fetchPageConfig({ product, verb }) { - try { - const url = `${unityConfig.pageConfigEndPoint}?product=${product}&verb=${verb}`; - const resp = await fetch(url, { headers: { 'x-api-key': unityConfig.apiKey } }); - if (!resp.ok) throw new Error(`PageConfig fetch failed: ${resp.statusText}`); - return resp.json(); - } catch (e) { - return {}; - } -} diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index f102ba72..013755c2 100644 --- a/unitylibs/utils/experiment-provider.js +++ b/unitylibs/utils/experiment-provider.js @@ -1,5 +1,11 @@ /* eslint-disable no-underscore-dangle */ +export async function getDecisionScopesForVerb(verb) { + const region = await getRegion().catch(() => undefined); + const verbScope = `acom_unity_acrobat_${verb}`; + return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; +} + export async function getRegion() { const resp = await fetch('https://geo2.adobe.com/json/', { cache: 'no-cache' }); if (!resp.ok) throw new Error(`Failed to resolve region: ${resp.statusText}`); @@ -8,13 +14,7 @@ export async function getRegion() { return country.toLowerCase(); } -export async function getDecisionScopesForVerb(verb) { - const region = await getRegion().catch(() => undefined); - const verbScope = `acom_unity_acrobat_${verb}`; - return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; -} - -export default async function getExperimentData(decisionScopes) { +export async function getExperimentData(decisionScopes) { if (!decisionScopes || decisionScopes.length === 0) { throw new Error('No decision scopes provided for experiment data fetch'); } From dbb7a0ad84ad2c3f090be8a24fe6f29e7243a307 Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:35:07 -0700 Subject: [PATCH 05/18] MWPW-191927 Implementing Config-Based A/B Testing (#756) --- test/utils/experiment-provider.test.js | 2 +- .../workflow-acrobat/action-binder.js | 56 +++++++++++++------ unitylibs/scripts/utils.js | 13 +++++ unitylibs/utils/experiment-provider.js | 14 ++--- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 3a397cac..25f1378f 100644 --- a/test/utils/experiment-provider.test.js +++ b/test/utils/experiment-provider.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { expect } from '@esm-bundle/chai'; -import { getExperimentData, getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; +import getExperimentData, { getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; describe('getExperimentData', () => { // Helper function to setup mock with result and error diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 7af73e30..90dd8c23 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -161,7 +161,7 @@ export default class ActionBinder { this.actionMap = actionMap; this.limits = {}; this.operations = []; - this.acrobatApiConfig = this.getAcrobatApiConfig(); + this.acrobatApiConfig = null; this.networkUtils = new NetworkUtils(); this.uploadHandler = null; this.splashScreenEl = null; @@ -182,6 +182,9 @@ export default class ActionBinder { this.multiFileValidationFailure = false; this.initialize(); this.experimentData = null; + this.experimentViaPageConfig = false; + this.pageConfigLocation = null; + this.pageConfigFetched = false; } async initialize() { @@ -224,11 +227,13 @@ export default class ActionBinder { } getAcrobatApiConfig() { + const base = this.pageConfigLocation ? `${this.pageConfigLocation}/api/v1` : unityConfig.apiEndPoint; unityConfig.acrobatEndpoint = { - createAsset: `${unityConfig.apiEndPoint}/asset`, - finalizeAsset: `${unityConfig.apiEndPoint}/asset/finalize`, - getMetadata: `${unityConfig.apiEndPoint}/asset/metadata`, + createAsset: `${base}/asset`, + finalizeAsset: `${base}/asset/finalize`, + getMetadata: `${base}/asset/metadata`, }; + unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; } @@ -242,18 +247,6 @@ export default class ActionBinder { } async handlePreloads() { - if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0])) { - const { getExperimentData, getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); - try { - const decisionScopes = await getDecisionScopesForVerb(this.workflowCfg.enabledFeatures[0]); - this.experimentData = await getExperimentData(decisionScopes); - } catch (error) { - await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { - code: 'warn_fetch_experiment', - desc: error.message, - }); - } - } const parr = []; if (this.workflowCfg.targetCfg.showSplashScreen) { parr.push( @@ -263,6 +256,32 @@ export default class ActionBinder { await priorityLoad(parr); } + async ensurePageConfig() { + if (this.pageConfigFetched) return; + this.pageConfigFetched = true; + const verb = this.workflowCfg.enabledFeatures[0]; + try { + const { fetchPageConfig } = await import('../../../scripts/utils.js'); + const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); + const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); + this.pageConfigLocation = pageConfig.location; + if (pageConfig.config?.target?.enabled) { + this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); + this.experimentViaPageConfig = true; + } else if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(verb)) { + const { getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); + const decisionScopes = await getDecisionScopesForVerb(verb); + this.experimentData = await getExperimentData(decisionScopes); + } + } catch (error) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + } + this.acrobatApiConfig = this.getAcrobatApiConfig(); + } + async dispatchErrorToast(errorType, status, info = null, lanaOnly = false, showError = true, errorMetaData = {}) { if (!showError) return; const errorMessage = errorType in this.workflowCfg.errors @@ -457,7 +476,7 @@ export default class ActionBinder { redirectUrl = url.href; } } - this.redirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; }) .catch(async (e) => { await this.showTransitionScreen(); @@ -486,7 +505,7 @@ export default class ActionBinder { if (this.multiFileValidationFailure) cOpts.payload.feedback = 'uploaderror'; if (this.showInfoToast) cOpts.payload.feedback = 'nonpdf'; } - if (this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]) && this.experimentData) { + if (this.experimentData && (this.experimentViaPageConfig || this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]))) { cOpts.payload.variationId = this.experimentData.variationId; } await this.getRedirectUrl(cOpts); @@ -556,6 +575,7 @@ export default class ActionBinder { if (prevalidatedFiles.length === 0) return; const { isValid, validFiles } = await this.validateFiles(prevalidatedFiles); if (!isValid) return; + await this.ensurePageConfig(); await this.initUploadHandler(); if (files.length === 1 || (validFiles.length === 1 && !verbsWithoutFallback.includes(this.workflowCfg.enabledFeatures[0]))) { await this.handleSingleFileUpload(validFiles); diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index b8fc7c57..ce6cbc44 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -297,12 +297,14 @@ export const unityConfig = (() => { prod: { apiEndPoint: 'https://unity.adobe.io/api/v1', connectorApiEndPoint: 'https://unity.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.adobe.com/api/v1/pageConfig', env: 'prod', ...commoncfg, }, stage: { apiEndPoint: 'https://unity-stage.adobe.io/api/v1', connectorApiEndPoint: 'https://unity-stage.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.stage.adobe.com/api/v1/pageConfig', env: 'stage', ...commoncfg, }, @@ -335,3 +337,14 @@ export function sendAnalyticsEvent(event) { export function getMatchedDomain(domainMap = {}, hostname = window.location.hostname) { return Object.keys(domainMap).find((domain) => domainMap[domain].some((pattern) => new RegExp(pattern).test(hostname))); } + +export async function fetchPageConfig({ product, verb }) { + try { + const url = `${unityConfig.pageConfigEndPoint}?product=${product}&verb=${verb}`; + const resp = await fetch(url, { headers: { 'x-api-key': unityConfig.apiKey } }); + if (!resp.ok) throw new Error(`PageConfig fetch failed: ${resp.statusText}`); + return resp.json(); + } catch (e) { + return {}; + } +} diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index 013755c2..f102ba72 100644 --- a/unitylibs/utils/experiment-provider.js +++ b/unitylibs/utils/experiment-provider.js @@ -1,11 +1,5 @@ /* eslint-disable no-underscore-dangle */ -export async function getDecisionScopesForVerb(verb) { - const region = await getRegion().catch(() => undefined); - const verbScope = `acom_unity_acrobat_${verb}`; - return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; -} - export async function getRegion() { const resp = await fetch('https://geo2.adobe.com/json/', { cache: 'no-cache' }); if (!resp.ok) throw new Error(`Failed to resolve region: ${resp.statusText}`); @@ -14,7 +8,13 @@ export async function getRegion() { return country.toLowerCase(); } -export async function getExperimentData(decisionScopes) { +export async function getDecisionScopesForVerb(verb) { + const region = await getRegion().catch(() => undefined); + const verbScope = `acom_unity_acrobat_${verb}`; + return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; +} + +export default async function getExperimentData(decisionScopes) { if (!decisionScopes || decisionScopes.length === 0) { throw new Error('No decision scopes provided for experiment data fetch'); } From 7a061692090382c93aeb4f30873051d5e95bdae3 Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:59:28 +0530 Subject: [PATCH 06/18] Revert "[MWPW-189488, MWPW-189974] : Bug fixes " (#758) Reverts adobecom/unity#742 --- .../workflow/workflow-upload/action-binder.js | 22 ++----------------- unitylibs/scripts/transition-screen.js | 13 +---------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index 9a773948..517bf8cb 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -345,11 +345,7 @@ export default class ActionBinder { } const finalResults = await Promise.allSettled(this.promiseStack); if (finalResults.some((result) => result.status === 'rejected')) return; - requestAnimationFrame(() => { - requestAnimationFrame(() => { - window.location.href = response.url; - }); - }); + window.location.href = response.url; } catch (e) { if (e.message === 'Operation termination requested.') return; await this.transitionScreen.showSplashScreen(); @@ -499,20 +495,7 @@ export default class ActionBinder { }); }, INPUT: (el, key) => { - let isFilePickerOpen = false; - el.addEventListener('click', (e) => { - if (isFilePickerOpen) { - e.preventDefault(); - return; - } - isFilePickerOpen = true; - const releaseFilePickerLock = () => { isFilePickerOpen = false; }; - window.addEventListener('focus', releaseFilePickerLock, { once: true }); - document.addEventListener('visibilitychange', function onPageVisible() { - if (document.visibilityState !== 'visible') return; - releaseFilePickerLock(); - document.removeEventListener('visibilitychange', onPageVisible); - }); + el.addEventListener('click', () => { this.canvasArea.forEach((element) => { const errHolder = element.querySelector('.alert-holder'); if (errHolder?.classList.contains('show')) { @@ -522,7 +505,6 @@ export default class ActionBinder { }); }); el.addEventListener('change', async (e) => { - isFilePickerOpen = false; const files = this.extractFiles(e); this.filesData = { count: files.length, size: files[0].size, type: files[0].type }; this.logAnalyticsinSplunk('Click Drag and drop|UnityWidget', { assetId: this.assetId, fileMetaData: this.filesData }); diff --git a/unitylibs/scripts/transition-screen.js b/unitylibs/scripts/transition-screen.js index f0d038df..800b1642 100644 --- a/unitylibs/scripts/transition-screen.js +++ b/unitylibs/scripts/transition-screen.js @@ -20,14 +20,6 @@ export default class TransitionScreen { this.isDesktop = isDesktop; this.headingElements = []; this.progressText = ''; - this._progressBarTimeout = null; - } - - cancelProgressBar() { - if (this._progressBarTimeout !== null) { - clearTimeout(this._progressBarTimeout); - this._progressBarTimeout = null; - } } setProgressTextFromDOM() { @@ -77,8 +69,7 @@ export default class TransitionScreen { if (currentValue === 100 || currentValue >= this.LOADER_LIMIT) return; } - this._progressBarTimeout = setTimeout(() => { - this._progressBarTimeout = null; + setTimeout(() => { const v = initialize ? 0 : parseInt(progressBar.getAttribute('value'), 10); if (v === 100) return; this.updateProgressBar(s, v + newI); @@ -216,7 +207,6 @@ export default class TransitionScreen { splashVisibilityController(displayOn) { if (!displayOn) { - this.cancelProgressBar(); this.LOADER_LIMIT = 95; this.splashScreenEl.parentElement?.classList.remove('hide-splash-overflow'); this.splashScreenEl.classList.remove('show'); @@ -225,7 +215,6 @@ export default class TransitionScreen { document.querySelector('footer').removeAttribute('aria-hidden'); return; } - this.cancelProgressBar(); this.progressBarHandler(this.splashScreenEl, this.LOADER_DELAY, this.LOADER_INCREMENT, true); this.resetSplashVideos(); this.splashScreenEl.classList.add('show'); From 9e5f123a7cc45bf4db992d451f629a72a7e37b51 Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:59:46 +0530 Subject: [PATCH 07/18] Revert "[MWPW-192702] Fixing issues observed from evaluating the errors in analytics dashboard. " (#757) Reverts adobecom/unity#740 --- .../workflow-upload/action-binder.test.js | 81 ++---- .../workflow-upload/upload-handler.test.js | 48 ++-- .../scripts/transition-screen.test.js | 260 ------------------ .../workflow/workflow-upload/action-binder.js | 26 +- .../workflow-upload/upload-handler.js | 26 +- unitylibs/utils/NetworkUtils.js | 16 +- unitylibs/utils/chunkingUtils.js | 8 +- 7 files changed, 72 insertions(+), 393 deletions(-) diff --git a/test/core/workflow/workflow-upload/action-binder.test.js b/test/core/workflow/workflow-upload/action-binder.test.js index 9f433687..6af3f09f 100644 --- a/test/core/workflow/workflow-upload/action-binder.test.js +++ b/test/core/workflow/workflow-upload/action-binder.test.js @@ -218,6 +218,24 @@ describe('Unity Upload Block', () => { } }); + it('should scan image for safety', async () => { + const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); + + actionBinder.serviceHandler = { postCallToService: async () => ({ status: 200 }) }; + + await actionBinder.scanImgForSafety('test-asset-id'); + }); + + it('should handle scan image for safety with retry', async () => { + fetchStub.resolves({ status: 429 }); + + const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); + + actionBinder.serviceHandler = { postCallToService: async () => ({ status: 429 }) }; + + await actionBinder.scanImgForSafety('test-asset-id'); + }); + it('should upload asset', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); @@ -529,55 +547,6 @@ describe('Unity Upload Block', () => { const files = actionBinder.extractFiles(mockEvent); expect(files).to.have.length(1); }); - - it('should return empty array when dataTransfer has no file items', () => { - const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - const mockEvent = { - dataTransfer: { items: [{ kind: 'string', getAsFile: () => null }] }, - }; - expect(actionBinder.extractFiles(mockEvent)).to.deep.equal([]); - }); - - it('should return empty array when target has no files', () => { - const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - const mockEvent = { dataTransfer: null, target: { files: [] } }; - expect(actionBinder.extractFiles(mockEvent)).to.deep.equal([]); - }); - }); - - describe('preventDefault helper', () => { - it('should call preventDefault and stopPropagation on the event', () => { - const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - const ev = { preventDefault: sinon.spy(), stopPropagation: sinon.spy() }; - actionBinder.preventDefault(ev); - expect(ev.preventDefault.calledOnce).to.be.true; - expect(ev.stopPropagation.calledOnce).to.be.true; - }); - }); - - describe('getAdditionalHeaders', () => { - it('should append verb to x-unity-action when verb is set', () => { - const wf = { - ...workflowCfg, - supportedFeatures: { values: () => ({ next: () => ({ value: 'remove-object' }) }) }, - }; - const actionBinder = new ActionBinder(unityEl, wf, unityEl, [unityEl]); - actionBinder.verb = 'v2'; - const headers = actionBinder.getAdditionalHeaders(); - expect(headers['x-unity-action']).to.equal('remove-object-v2'); - expect(headers['x-unity-product']).to.equal(wf.productName); - }); - - it('should use base action only when verb is unset', () => { - const wf = { - ...workflowCfg, - supportedFeatures: { values: () => ({ next: () => ({ value: 'upload-only' }) }) }, - }; - const actionBinder = new ActionBinder(unityEl, wf, unityEl, [unityEl]); - actionBinder.verb = undefined; - const headers = actionBinder.getAdditionalHeaders(); - expect(headers['x-unity-action']).to.equal('upload-only'); - }); }); describe('Action Maps and Event Handling', () => { @@ -655,20 +624,6 @@ describe('Unity Upload Block', () => { await actionBinder.executeActionMaps('unknown', []); }); - it('should log analytics for redirect action', async () => { - const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - actionBinder.transitionScreen = { splashScreenEl: document.createElement('div') }; - actionBinder.errorToastEl = document.createElement('div'); - sinon.stub(actionBinder, 'loadTransitionScreen').resolves(); - sinon.stub(actionBinder, 'handlePreloads').resolves(); - const spy = sinon.stub(actionBinder, 'logAnalyticsinSplunk'); - await actionBinder.executeActionMaps('redirect'); - expect(spy.calledWith('Edit Photos CTA|UnityWidget', {})).to.be.true; - spy.restore(); - actionBinder.loadTransitionScreen.restore(); - actionBinder.handlePreloads.restore(); - }); - it('should initialize action listeners', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); diff --git a/test/core/workflow/workflow-upload/upload-handler.test.js b/test/core/workflow/workflow-upload/upload-handler.test.js index e998e7bc..f3496efd 100644 --- a/test/core/workflow/workflow-upload/upload-handler.test.js +++ b/test/core/workflow/workflow-upload/upload-handler.test.js @@ -209,13 +209,12 @@ describe('UploadHandler', () => { }); it('should handle abort signal', async () => { - const controller = new AbortController(); - controller.abort(); - const signal = controller.signal; + const signal = { aborted: true }; const file = new File(['test data'], 'test.txt', { type: 'text/plain' }); const uploadUrls = ['http://upload1.com']; const blockSize = 20; + // Set up fetch stub window.fetch = sinon.stub(); const result = await uploadHandler.uploadChunksToUnity(uploadUrls, file, blockSize, signal); @@ -268,19 +267,19 @@ describe('UploadHandler', () => { }); describe('uploadFileToUnity Error Handling', () => { - let originalFetchFromServiceWithRetry; + let originalFetch; beforeEach(() => { - originalFetchFromServiceWithRetry = uploadHandler.networkUtils.fetchFromServiceWithRetry; + originalFetch = window.fetch; }); afterEach(() => { - uploadHandler.networkUtils.fetchFromServiceWithRetry = originalFetchFromServiceWithRetry; + window.fetch = originalFetch; }); it('should handle upload failure with no statusText', async () => { - const retryError = new Error('Upload request failed, Max retry delay exceeded for URL: http://upload.com'); - uploadHandler.networkUtils.fetchFromServiceWithRetry = sinon.stub().rejects(retryError); + const mockResponse = { ok: false, status: 500 }; + window.fetch = sinon.stub().resolves(mockResponse); const blob = new Blob(['test data'], { type: 'text/plain' }); @@ -295,7 +294,7 @@ describe('UploadHandler', () => { it('should handle AbortError during upload', async () => { const abortError = new Error('Request aborted'); abortError.name = 'AbortError'; - uploadHandler.networkUtils.fetchFromServiceWithRetry = sinon.stub().rejects(abortError); + window.fetch = sinon.stub().rejects(abortError); const blob = new Blob(['test data'], { type: 'text/plain' }); @@ -303,14 +302,14 @@ describe('UploadHandler', () => { await uploadHandler.uploadFileToUnity('http://upload.com', blob, 'text/plain', 'asset-123'); expect.fail('Should have thrown AbortError'); } catch (error) { - expect(error.name).to.equal('AbortError'); + expect(error.message).to.include('Max retry delay exceeded'); } }); it('should handle Timeout error during upload', async () => { - const timeoutError = new Error('Request timed out after 60000ms, Max retry delay exceeded for URL: http://upload.com'); - timeoutError.name = 'TimeoutError'; - uploadHandler.networkUtils.fetchFromServiceWithRetry = sinon.stub().rejects(timeoutError); + const timeoutError = new Error('Request timed out'); + timeoutError.name = 'Timeout'; + window.fetch = sinon.stub().rejects(timeoutError); const blob = new Blob(['test data'], { type: 'text/plain' }); @@ -318,40 +317,29 @@ describe('UploadHandler', () => { await uploadHandler.uploadFileToUnity('http://upload.com', blob, 'text/plain', 'asset-123'); expect.fail('Should have thrown error'); } catch (error) { - expect(error.message).to.include('Request timed out'); + expect(error.message).to.include('Max retry delay exceeded'); } }); }); describe('uploadChunksToUnity Error Handling', () => { - let originalUploadFileToUnity; + let originalFetch; beforeEach(() => { - originalUploadFileToUnity = uploadHandler.uploadFileToUnity; + originalFetch = window.fetch; }); afterEach(() => { - uploadHandler.uploadFileToUnity = originalUploadFileToUnity; + window.fetch = originalFetch; }); it('should log chunk errors when upload fails', async () => { - const networkError = new Error('Network error'); - networkError.status = 0; - - uploadHandler.uploadFileToUnity = async () => { - mockActionBinder.logAnalyticsinSplunk('Upload Chunk Error|UnityWidget', { - errorData: { code: 'upload-error-chunk-upload', subCode: 0, desc: 'Network error' }, - assetId: mockActionBinder.assetId, - }); - throw networkError; - }; - + const mockError = new Error('Network error'); + window.fetch = sinon.stub().rejects(mockError); const file = new File(['test data'], 'test.txt', { type: 'text/plain' }); const uploadUrls = ['http://upload1.com']; const blockSize = 10; - const result = await uploadHandler.uploadChunksToUnity(uploadUrls, file, blockSize); - expect(result.failedChunks.size).to.equal(1); expect(mockActionBinder.logAnalyticsinSplunk.calledWith('Upload Chunk Error|UnityWidget')).to.be.true; expect(mockActionBinder.logAnalyticsinSplunk.calledWith('Chunked Upload Failed|UnityWidget')).to.be.true; diff --git a/test/unitylibs/scripts/transition-screen.test.js b/test/unitylibs/scripts/transition-screen.test.js index 397ec490..472c3582 100644 --- a/test/unitylibs/scripts/transition-screen.test.js +++ b/test/unitylibs/scripts/transition-screen.test.js @@ -33,36 +33,6 @@ describe('TransitionScreen', () => { expect(fill.style.width).to.equal('42%'); expect(status.textContent).to.equal('42%'); }); - - it('should no-op when layer is null', () => { - expect(() => screen.updateProgressBar(null, 50)).to.not.throw(); - }); - - it('should restore progressText from lastProgressText when empty', () => { - TransitionScreen.lastProgressText = 'Uploading %'; - screen.progressText = ''; - splashScreenEl.innerHTML = ` -
-
0%
-
-
- `; - screen.updateProgressBar(splashScreenEl, 10); - const status = splashScreenEl.querySelector('#progress-status'); - expect(status.textContent).to.equal('Uploading 10%'); - }); - - it('should cap percentage at LOADER_LIMIT', () => { - screen.LOADER_LIMIT = 80; - splashScreenEl.innerHTML = ` -
-
0%
-
-
- `; - screen.updateProgressBar(splashScreenEl, 99); - expect(splashScreenEl.querySelector('.spectrum-ProgressBar').getAttribute('value')).to.equal('80'); - }); }); describe('createProgressBar', () => { @@ -91,52 +61,6 @@ describe('TransitionScreen', () => { spy.restore(); clock.restore(); }); - - it('should return early when splash element is missing', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(screen, 'updateProgressBar'); - screen.progressBarHandler(null, 10, 10, true); - clock.tick(100); - expect(spy.called).to.be.false; - spy.restore(); - clock.restore(); - }); - - it('should return early when current value already at LOADER_LIMIT', () => { - screen.LOADER_LIMIT = 70; - splashScreenEl.innerHTML = ` -
-
70%
-
-
- `; - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(screen, 'updateProgressBar'); - screen.progressBarHandler(splashScreenEl, 10, 10, false); - clock.tick(100); - expect(spy.called).to.be.false; - spy.restore(); - clock.restore(); - }); - - it('should return early inside timeout when value is 100', () => { - splashScreenEl.innerHTML = ` -
-
0%
-
-
- `; - const clock = sinon.useFakeTimers(); - const stub = sinon.stub(screen, 'updateProgressBar').callsFake((layer, pct) => { - if (pct === 10) { - layer.querySelector('.spectrum-ProgressBar').setAttribute('value', '100'); - } - }); - screen.progressBarHandler(splashScreenEl, 10, 10, true); - clock.tick(20); - stub.restore(); - clock.restore(); - }); }); describe('handleSplashProgressBar', () => { @@ -186,20 +110,6 @@ describe('TransitionScreen', () => { expect(document.querySelector('main').getAttribute('aria-hidden')).to.equal('true'); stub.restore(); }); - - it('should focus splash element after short delay when shown', () => { - const stubPb = sinon.stub(screen, 'progressBarHandler'); - sinon.stub(screen, 'resetSplashVideos'); - splashScreenEl.setAttribute('tabindex', '-1'); - splashScreenEl.focus = sinon.spy(); - const clock = sinon.useFakeTimers(); - screen.splashVisibilityController(true); - clock.tick(50); - expect(splashScreenEl.focus.calledOnce).to.be.true; - stubPb.restore(); - screen.resetSplashVideos.restore(); - clock.restore(); - }); }); describe('updateCopyForDevice', () => { @@ -305,175 +215,5 @@ describe('TransitionScreen', () => { }; expect(screen.getFragmentLink(undefined)).to.equal(fragmentLink); }); - - it('should return themed fragment link for workflow-upload when theme is set', () => { - screen.workflowCfg = { - name: 'workflow-upload', - theme: 'dark', - productName: 'Firefly', - targetCfg: { - splashScreenConfig: { - fragmentLink: '/default', - 'fragmentLink-firefly': '/ff-light', - 'fragmentLink-firefly-dark': '/ff-dark', - }, - }, - }; - expect(screen.getFragmentLink(undefined)).to.equal('/ff-dark'); - }); - - it('should fall back to product fragment when theme set but no themed key exists', () => { - screen.workflowCfg = { - name: 'workflow-upload', - theme: 'dark', - productName: 'Firefly', - targetCfg: { - splashScreenConfig: { - fragmentLink: '/default', - 'fragmentLink-firefly': '/ff-only', - }, - }, - }; - expect(screen.getFragmentLink(undefined)).to.equal('/ff-only'); - }); - }); - - describe('checkForProgressBar', () => { - it('should return icon-progress-bar element when present', () => { - const icon = document.createElement('div'); - icon.className = 'icon-progress-bar'; - splashScreenEl.appendChild(icon); - expect(screen.checkForProgressBar()).to.equal(icon); - }); - - it('should build progress bar from [[progress-bar]] placeholder paragraph', () => { - const p = document.createElement('p'); - p.textContent = 'Status [[progress-bar]] more text'; - splashScreenEl.appendChild(p); - const result = screen.checkForProgressBar(); - expect(result.classList.contains('progress-bar-area')).to.be.true; - expect(result.querySelector('.progress-bar')).to.exist; - expect(p.textContent).to.not.include('[[progress-bar]]'); - }); - - it('should return null when no progress UI markers exist', () => { - splashScreenEl.innerHTML = '

Plain copy only

'; - expect(screen.checkForProgressBar()).to.be.null; - }); - }); - - describe('setProgressTextFromDOM', () => { - it('should collect text nodes next to progress-bar span', () => { - splashScreenEl.innerHTML = ` -
- - Uploading your file -
`; - const nodes = screen.setProgressTextFromDOM(); - expect(nodes.length).to.be.at.least(1); - expect(screen.progressText).to.include('Uploading'); - }); - }); - - describe('resetSplashVideos', () => { - it('should return when splashScreenEl is missing', () => { - screen.splashScreenEl = null; - expect(() => screen.resetSplashVideos()).to.not.throw(); - }); - - it('should reset and play video when readyState is high enough', () => { - const video = document.createElement('video'); - Object.defineProperty(video, 'readyState', { value: 4, configurable: true }); - sinon.stub(video, 'play').resolves(); - sinon.stub(video, 'load'); - splashScreenEl.appendChild(video); - screen.resetSplashVideos(); - expect(video.load.called).to.be.true; - video.play.restore(); - video.load.restore(); - }); - - it('should wait for canplay when video is not ready', () => { - const video = document.createElement('video'); - Object.defineProperty(video, 'readyState', { value: 0, configurable: true }); - sinon.stub(video, 'load'); - sinon.stub(video, 'play').resolves(); - splashScreenEl.appendChild(video); - screen.resetSplashVideos(); - video.dispatchEvent(new Event('canplay')); - expect(video.load.called).to.be.true; - video.play.restore(); - video.load.restore(); - }); - }); - - describe('loadSplashFragment', () => { - it('should return immediately when showSplashScreen is false', async () => { - screen.workflowCfg = { - targetCfg: { showSplashScreen: false, splashScreenConfig: { splashScreenParent: 'body' } }, - productName: 'test', - name: 'workflow-upload', - }; - const fetchSpy = sinon.spy(window, 'fetch'); - await screen.loadSplashFragment(); - expect(fetchSpy.called).to.be.false; - fetchSpy.restore(); - }); - }); - - describe('delayedSplashLoader', () => { - it('should call loadSplashFragment after idle timeout', async () => { - const stub = sinon.stub(screen, 'loadSplashFragment').resolves(); - const clock = sinon.useFakeTimers(); - screen.delayedSplashLoader(); - clock.tick(8000); - await Promise.resolve(); - await Promise.resolve(); - expect(stub.calledOnce).to.be.true; - stub.restore(); - clock.restore(); - }); - - it('should call loadSplashFragment on first pointer interaction', async () => { - const stub = sinon.stub(screen, 'loadSplashFragment').resolves(); - screen.delayedSplashLoader(); - document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true })); - await Promise.resolve(); - await Promise.resolve(); - expect(stub.calledOnce).to.be.true; - stub.restore(); - }); - }); - - describe('showSplashScreen photoshop branch', () => { - it('should call updateCopyForDevice when product is Photoshop', async () => { - splashScreenEl.classList.add('decorate'); - const icon = document.createElement('div'); - icon.className = 'icon-progress-bar'; - splashScreenEl.appendChild(icon); - const h0 = document.createElement('h1'); - h0.innerText = 'H0'; - const h1 = document.createElement('h2'); - h1.innerText = 'H1'; - const h2 = document.createElement('h3'); - h2.innerText = 'H2'; - const h3 = document.createElement('h4'); - h3.innerText = 'H3'; - splashScreenEl.appendChild(h0); - splashScreenEl.appendChild(h1); - splashScreenEl.appendChild(h2); - splashScreenEl.appendChild(h3); - screen.workflowCfg = { - targetCfg: { showSplashScreen: true }, - productName: 'Photoshop', - name: 'workflow-upload', - }; - const stub = sinon.stub(screen, 'updateCopyForDevice'); - const stubPb = sinon.stub(screen, 'handleSplashProgressBar').resolves(); - await screen.showSplashScreen(); - expect(stub.called).to.be.true; - stub.restore(); - stubPb.restore(); - }); }); }); diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index 517bf8cb..d74726f9 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -186,6 +186,30 @@ export default class ActionBinder { } } + async scanImgForSafety(assetId, signal) { + if (signal?.aborted) { + const err = new Error('Operation aborted'); + err.name = 'AbortError'; + throw err; + } + const assetData = { assetId, targetProduct: this.workflowCfg.productName }; + const optionsBody = { body: JSON.stringify(assetData), ...(signal && { signal }) }; + const res = await this.serviceHandler.postCallToService( + this.apiConfig.endPoint.acmpCheck, + optionsBody, + {}, + false, + ); + if (res.status === 429 || (res.status >= 500 && res.status < 600)) { + if (signal?.aborted) { + const err = new Error('Operation aborted'); + err.name = 'AbortError'; + throw err; + } + setTimeout(() => { this.scanImgForSafety(assetId, signal); }, 1000); + } + } + async uploadAsset(file) { const assetDetails = { targetProduct: this.workflowCfg.productName, @@ -233,7 +257,7 @@ export default class ActionBinder { ); } else { await this.uploadImgToUnity(href, id, file, file.type, signal); - await uploadHandler.scanImgForSafetyWithRetry(this.assetId, signal); + await this.scanImgForSafety(this.assetId, signal); this.logAnalyticsinSplunk('Upload Completed|UnityWidget', { assetId: this.assetId }); } return true; diff --git a/unitylibs/core/workflow/workflow-upload/upload-handler.js b/unitylibs/core/workflow/workflow-upload/upload-handler.js index f14ed0fa..57d5bf29 100644 --- a/unitylibs/core/workflow/workflow-upload/upload-handler.js +++ b/unitylibs/core/workflow/workflow-upload/upload-handler.js @@ -46,37 +46,24 @@ export default class UploadHandler { error.status = response.status; throw error; }; - const onError = (error, attempt) => { + const onError = (error) => { if (error.name !== 'AbortError') { - const isFinalAttempt = attempt >= retryConfig.retryParams.maxRetries; - const eventName = isFinalAttempt ? 'Upload Chunk Error|UnityWidget' : 'Upload Chunk Warn|UnityWidget'; - const code = isFinalAttempt ? 'upload-error-chunk-upload' : 'upload-warn-chunk-upload'; - this.logError(eventName, { + this.logError('Upload Chunk Error|UnityWidget', { chunkNumber, size: blobData.size, fileType, errorData: { - code, - subCode: error.status ?? (error.name === 'TimeoutError' ? 504 : 0), - desc: `Exception during chunk ${chunkNumber} upload (attempt ${attempt}): ${error.message}`, + code: 'upload-chunk-error', + desc: `Exception during chunk ${chunkNumber} upload: ${error.message}`, }, }, `Message: Exception raised when uploading chunk to Unity, Error: ${error.message}, Asset ID: ${assetId}, ${blobData.size} bytes`); - if (isFinalAttempt) { - this.chunkAbortController?.abort(); - throw error; - } - } else { - throw error; } + throw error; }; return this.networkUtils.fetchFromServiceWithRetry(storageUrl, uploadOptions, retryConfig, onSuccess, onError); } async uploadChunksToUnity(uploadUrls, file, blockSize, signal = null) { - this.chunkAbortController = new AbortController(); - const mergedSignal = signal - ? AbortSignal.any([signal, this.chunkAbortController.signal]) - : this.chunkAbortController.signal; const options = { assetId: this.actionBinder.assetId, fileType: file.type, @@ -86,10 +73,9 @@ export default class UploadHandler { file, blockSize, this.uploadFileToUnity.bind(this), - mergedSignal, + signal, options, ); - this.chunkAbortController = null; const { failedChunks, attemptMap } = result; const totalChunks = Math.ceil(file.size / blockSize); if (failedChunks.size > 0 && !signal?.aborted) { diff --git a/unitylibs/utils/NetworkUtils.js b/unitylibs/utils/NetworkUtils.js index 82432979..555628b9 100644 --- a/unitylibs/utils/NetworkUtils.js +++ b/unitylibs/utils/NetworkUtils.js @@ -10,10 +10,8 @@ export default class NetworkUtils { async fetchWithTimeout(url, options = {}, timeoutMs = 60000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); - const mergedSignal = options.signal - ? AbortSignal.any([controller.signal, options.signal]) - : controller.signal; - const mergedOptions = { ...options, signal: mergedSignal }; + const passedSignal = options.signal || controller.signal; + const mergedOptions = { ...options, signal: passedSignal }; try { const response = await fetch(url, mergedOptions); clearTimeout(timeout); @@ -21,7 +19,6 @@ export default class NetworkUtils { } catch (e) { clearTimeout(timeout); if (e.name === 'AbortError') { - if (options.signal?.aborted) throw e; const error = new Error(`Request timed out after ${timeoutMs}ms`); error.name = 'TimeoutError'; throw error; @@ -104,15 +101,6 @@ export default class NetworkUtils { const onSuccessWithAttempt = onSuccess ? (response) => onSuccess(response, attempt) : null; const onErrorWithAttempt = onError ? (error) => onError(error, attempt) : null; let response = await this.fetchFromService(url, options, onSuccessWithAttempt, onErrorWithAttempt); - if (!response) { - if (attempt < maxRetries) { - const delay = retryDelay; - await new Promise((resolve) => { setTimeout(resolve, delay); }); - retryDelay *= 2; - continue; - } - break; - } const customRetryCheckResult = retryConfig.extraRetryCheck && await retryConfig.extraRetryCheck(response); if ((customRetryCheckResult || response.status === 202 || (response.status >= 500 && response.status < 600) || response.status === 429)) { if (attempt < maxRetries) { diff --git a/unitylibs/utils/chunkingUtils.js b/unitylibs/utils/chunkingUtils.js index 91fc1bcf..21bb554a 100644 --- a/unitylibs/utils/chunkingUtils.js +++ b/unitylibs/utils/chunkingUtils.js @@ -48,11 +48,9 @@ export async function createChunkUploadTasks(uploadUrls, file, blockSize, upload if (onChunkComplete) onChunkComplete(i, chunkNumber, result); return result; } catch (err) { - if (err.name !== 'AbortError') { - const chunkInfo = { chunkIndex: i, chunkNumber }; - failedChunks.add(chunkInfo); - if (onChunkError) onChunkError(chunkInfo, err); - } + const chunkInfo = { chunkIndex: i, chunkNumber }; + failedChunks.add(chunkInfo); + if (onChunkError) onChunkError(chunkInfo, err); throw err; } })(); From 008bbc21e290d33ffca5aff0c4413a84734c0f50 Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:18:14 +0530 Subject: [PATCH 08/18] [MWPW-193670] Wave2.1 - Image to video changes (#751) Testing Notes: 1. Regress all the upload workflow acros products 2. Test the limits for each product Resolves: [MWPW-193670](https://jira.corp.adobe.com/browse/MWPW-193670) **Test URLs:** - Before: https://main--da-cc--adobecom.aem.page/drafts/arugupta/image-to-video/doodlebug/image-to-video?unitylibs=stage - After: https://main--da-cc--adobecom.aem.page/drafts/arugupta/image-to-video/doodlebug/image-to-video?unitylibs=wave2-video --------- Co-authored-by: Arushi Gupta Co-authored-by: Vipul Gupta Co-authored-by: vipulg --- .../workflow-upload/action-binder.test.js | 30 +- .../prompt-bar-style/prompt-bar-style.css | 2 +- .../prompt-bar-upload/prompt-bar-upload.css | 918 ++++++++++++++++++ .../prompt-bar-upload/prompt-bar-upload.js | 607 ++++++++++++ .../action-binder.js | 733 ++++++++++++++ .../workflow-prompt-bar-upload/sprite.svg | 46 + .../target-config.json | 31 + .../workflow/workflow-upload/action-binder.js | 48 +- .../workflow-upload/target-config.json | 6 + unitylibs/core/workflow/workflow.js | 33 +- unitylibs/img/icons/upload.svg | 4 + unitylibs/scripts/analytics.js | 14 +- unitylibs/scripts/transition-screen.js | 4 +- unitylibs/utils/FileUtils.js | 17 + 14 files changed, 2449 insertions(+), 44 deletions(-) create mode 100644 unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css create mode 100644 unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js create mode 100644 unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js create mode 100644 unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg create mode 100644 unitylibs/core/workflow/workflow-prompt-bar-upload/target-config.json create mode 100755 unitylibs/img/icons/upload.svg diff --git a/test/core/workflow/workflow-upload/action-binder.test.js b/test/core/workflow/workflow-upload/action-binder.test.js index 6af3f09f..28b5b6c1 100644 --- a/test/core/workflow/workflow-upload/action-binder.test.js +++ b/test/core/workflow/workflow-upload/action-binder.test.js @@ -463,7 +463,7 @@ describe('Unity Upload Block', () => { actionBinder.serviceHandler = { showErrorToast: () => {} }; const invalidFile = new File(['test content'], 'test.txt', { type: 'text/plain' }); - await actionBinder.uploadImage([invalidFile]); + await actionBinder.uploadFile([invalidFile]); }); it('should show error for file size exceeding limit', async () => { @@ -484,7 +484,7 @@ describe('Unity Upload Block', () => { actionBinder.serviceHandler = { showErrorToast: () => {} }; const largeFile = new File(['x'.repeat(2000)], 'large.jpg', { type: 'image/jpeg' }); - await actionBinder.uploadImage([largeFile]); + await actionBinder.uploadFile([largeFile]); }); it('should show error for wrong number of files', async () => { @@ -496,13 +496,13 @@ describe('Unity Upload Block', () => { new File(['test1'], 'test1.jpg', { type: 'image/jpeg' }), new File(['test2'], 'test2.jpg', { type: 'image/jpeg' }), ]; - await actionBinder.uploadImage(files); + await actionBinder.uploadFile(files); }); it('should handle null files', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - await actionBinder.uploadImage(null); + await actionBinder.uploadFile(null); }); }); @@ -978,7 +978,7 @@ describe('Unity Upload Block', () => { const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }); try { - await actionBinder.uploadImage([file]); + await actionBinder.uploadFile([file]); expect.fail('Should have thrown an error due to missing URL'); } catch (error) { expect(error.message).to.equal('Error connecting to App'); @@ -1169,13 +1169,13 @@ describe('Unity Upload Block', () => { expect(files).to.have.length(0); }); - it('should handle uploadImage with null files', async () => { + it('should handle uploadFile with null files', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); - await actionBinder.uploadImage(null); + await actionBinder.uploadFile(null); }); - it('should handle uploadImage with wrong number of files', async () => { + it('should handle uploadFile with wrong number of files', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); actionBinder.serviceHandler = { showErrorToast: () => {} }; @@ -1185,20 +1185,20 @@ describe('Unity Upload Block', () => { new File(['test2'], 'test2.jpg', { type: 'image/jpeg' }), ]; - await actionBinder.uploadImage(files); + await actionBinder.uploadFile(files); }); - it('should handle uploadImage with invalid file type', async () => { + it('should handle uploadFile with invalid file type', async () => { const actionBinder = new ActionBinder(unityEl, workflowCfg, unityEl, [unityEl]); actionBinder.serviceHandler = { showErrorToast: () => {} }; const files = [new File(['test'], 'test.txt', { type: 'text/plain' })]; - await actionBinder.uploadImage(files); + await actionBinder.uploadFile(files); }); - it('should handle uploadImage with file size exceeding limit', async () => { + it('should handle uploadFile with file size exceeding limit', async () => { const testWorkflowCfg = { productName: 'test-product', targetCfg: { @@ -1217,10 +1217,10 @@ describe('Unity Upload Block', () => { const files = [new File(['x'.repeat(2000)], 'test.jpg', { type: 'image/jpeg' })]; - await actionBinder.uploadImage(files); + await actionBinder.uploadFile(files); }); - it('should handle uploadImage with PSW feature enabled', async () => { + it('should handle uploadFile with PSW feature enabled', async () => { const testWorkflowCfg = { ...workflowCfg, pswFeature: true, @@ -1256,7 +1256,7 @@ describe('Unity Upload Block', () => { actionBinder.continueInApp = async () => Promise.resolve(); const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }); - await actionBinder.uploadImage([file]); + await actionBinder.uploadFile([file]); window.fetch = originalFetch; actionBinder.checkImageDimensions = originalCheckImageDimensions; diff --git a/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.css b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.css index c743beda..b7dd5bcb 100644 --- a/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.css +++ b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.css @@ -1333,4 +1333,4 @@ font-size: 64px; letter-spacing: -1.92px; } -} +} \ No newline at end of file diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css new file mode 100644 index 00000000..b29feb7c --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css @@ -0,0 +1,918 @@ +.upload-marquee.unity-enabled .interactive-area { + display: inherit; + background: none; +} + +.unity-prompt-bar-upload.unity-enabled { +width: 100%; +max-width: 1000px; +margin-top: 24px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area { +width: 727px; +background: #222 !important; +border: none !important; +box-shadow: none !important; +border-radius: 20px !important; +padding: 8px !important; +box-sizing: border-box !important; +max-width: 727px; +} + +@media screen and (min-width: 1200px) { +.unity-prompt-bar-upload.unity-enabled .interactive-area { + padding: 14px !important; +} +} + +.ex-unity-wrap.pbu-widget .pbu-legal-foot { +font-size: 14px; +width: fit-content; +max-width: 727px; +box-sizing: border-box; +font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; +font-size: 13px; +font-weight: 400; +line-height: 1.45; +color: #f8f8f8; +text-align: start; +margin-top: 12px; +} + +.ex-unity-wrap.pbu-widget .pbu-legal-foot a { +color: #1473E6; +text-decoration: none; +} + +.ex-unity-wrap.pbu-widget .pbu-legal-foot a:hover { +text-decoration: underline; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-main { +position: relative; +padding: 16px; +border-radius: 10px; +background: #1B1B1B; +display: flex; +flex-direction: row; +align-items: flex-start; +gap: 16px; +width: 100%; +box-sizing: border-box; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-left-section { +display: flex; +flex-direction: column; +align-items: flex-start; +gap: 17px; +flex-shrink: 0; +border-right: 1px solid rgb(255 255 255 / 12%); +padding-right: 16px; +box-sizing: border-box; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section { +display: flex; +flex-direction: column; +align-items: stretch; +gap: 0; +flex: 1; +min-width: 0; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .pbu-prompt-bar-container { +display: flex; +flex-direction: column; +gap: 0; +width: 100%; +min-width: 0; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-main .alert-holder { +position: absolute; +inset: 0; +width: 100%; +height: 100%; +display: none; +align-items: center; +justify-content: center; +padding: 12px; +box-sizing: border-box; +z-index: 350; +border-radius: 10px; +background: rgb(0 0 0 / 60%); +pointer-events: none; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-main .alert-holder.show { +display: flex; +pointer-events: auto; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-main .alert-holder .alert-toast { +position: relative; +left: auto; +right: auto; +top: auto; +bottom: auto; +margin: 0 auto; +width: min(339px, 100%); +max-width: 100%; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .unity-slf-prompt-label { +margin-bottom: 0px; +padding: 0; +display: block; +flex-shrink: 0; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field { +flex: 1; +min-height: 80px; +resize: none; +width: 100%; +box-sizing: border-box; +padding-bottom: 10px; +margin: 0 0 12px 0; +border: none; +background: transparent; +outline: none; +font-size: var(--type-body-s-size, 15px); +line-height: 1.45; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer { +display: flex; +flex-direction: row; +align-items: center; +gap: 8px; +width: 100%; +box-sizing: border-box; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container { +display: flex !important; +flex-wrap: wrap; +align-items: center; +gap: 8px; +justify-content: flex-start; +flex: 1; +margin-top: 0 !important; +min-width: 0; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap { +display: flex !important; +align-items: center; +gap: 10px; +justify-content: flex-end; +margin-top: 0 !important; +flex-shrink: 0; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container:empty ~ .act-wrap { +flex: 1; +justify-content: flex-end; +} + +.unity-prompt-bar-upload.unity-enabled .unity-slf-copy-label { +color: #d1d1d1; +font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; +font-size: 14px; +font-weight: 400; +line-height: 18px; +} + +.unity-prompt-bar-upload.unity-enabled .unity-slf-prompt-label { +display: block; +margin-bottom: 8px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .inp-field, +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .inp-field::placeholder { +color: #f8f8f8 !important; +margin-bottom: 18px; +font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; +font-size: 16px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .inp-field, +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .inp-field::placeholder { +color: #292929; +} + +@keyframes pbu-model-fade-in { +0% { opacity: 0; } +100% { opacity: 1; } +} + +@keyframes pbu-model-move-down { +0% { + transform: translateY(33px); + opacity: 0; + display: none; +} +100% { + transform: translateY(40px); + opacity: 1; +} +} + +@keyframes pbu-model-move-up { +0% { + transform: translateY(40px); + opacity: 1; +} +100% { + transform: translateY(33px); + opacity: 0; + display: none; +} +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model img, +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link img { +width: 22px; +height: 22px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon, +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon { +font-size: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon, +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon svg, +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon, +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon svg { +width: 12px; +height: 12px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container, +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .models-container { +display: flex; +flex: 0 1 auto; +max-width: none; +width: auto; +position: relative; +align-items: center; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .pbu-controls-footer .models-container { +max-width: none; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .verb-list .verb-link, +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .verb-list .verb-link .model-name { +color: #292929; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container:not(.pbu-aspect-models) .verb-list { +min-width: 270px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container .menu-icon { +position: relative; +top: 1px; +display: flex; +align-items: center; +justify-content: center; +flex-shrink: 0; +transition: transform 0.15s ease-in; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .menu-icon { +transform: rotate(-180deg); +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model { +display: inline-flex; +align-items: center; +gap: 8px; +justify-content: flex-start; +padding: 8px 12px; +min-height: 32px; +width: auto; +min-width: 27px; +max-width: 200px; +box-sizing: border-box; +cursor: pointer; +border: none; +font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; +font-size: 14px; +font-weight: 400; +line-height: 1.2; +text-transform: none; +white-space: nowrap; +background: #353535; +color: #fff; +border-radius: 10px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model:hover { +background: #434343; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model:focus-visible { +outline: 2px solid #2680eb; +outline-offset: 2px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model .model-name { +color: #fff; +overflow: hidden; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model .menu-icon svg, +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .models-container .menu-icon svg { +filter: brightness(0) invert(1); +opacity: 0.95; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model img { +width: 20px; +height: 20px; +border-radius: 6px; +flex-shrink: 0; +object-fit: cover; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-aspect-models .selected-model:not(:has(img))::before { +content: ''; +display: block; +width: 22px; +height: 12px; +box-sizing: border-box; +border: 1.5px solid rgb(255 255 255 / 92%); +border-radius: 2px; +flex-shrink: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model { +display: inline-flex; +align-items: center; +gap: 8px; +justify-content: flex-start; +padding: 8px 12px; +min-height: 40px; +width: auto; +border: none; +border-radius: 10px; +background: rgb(0 0 0 / 8%); +color: #292929; +font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; +font-size: 14px; +font-weight: 400; +cursor: pointer; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model .model-name { +color: #292929; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model .menu-icon svg, +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .models-container .menu-icon svg { +filter: brightness(0); +opacity: 0.85; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-aspect-models .selected-model:not(:has(img))::before { +content: ''; +display: block; +width: 22px; +height: 12px; +box-sizing: border-box; +border: 1.5px solid rgb(0 0 0 / 55%); +border-radius: 2px; +flex-shrink: 0; +} + +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model, +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model { +border-radius: 12px; +min-height: 36px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list { +padding: 18px; +list-style: none; +box-shadow: 0 0 10px #0000001c; +border-radius: 10px; +background: rgb(255 255 255 / 100%); +color: #292929; +margin: 0; +min-width: 110px; +animation: pbu-model-move-up 0.2s ease forwards; +position: absolute; +top: 0; +left: 0; +z-index: 1; +} + +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list { +border-radius: 14px; +} + +[lang="ja-JP"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list, +[lang="ko-KR"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list { +margin-top: 6px; +} + +[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list { +left: unset; +right: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .verb-list { +display: block; +animation: pbu-model-move-down 0.4s cubic-bezier(0.5, 1.8, 0.3, 0.8) forwards; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link { +display: flex; +align-items: center; +gap: 10px; +padding: 10px; +padding-inline-start: 25px; +text-transform: capitalize; +text-decoration: none; +text-align: start; +position: relative; +opacity: 0; +animation: pbu-model-fade-in 0.5s ease forwards; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .models-container .verb-list, +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verbs-container .verb-list { +background: #000; +color: #f8f8f8; +box-shadow: 0 8px 32px rgb(0 0 0 / 55%); +border: 1px solid rgb(255 255 255 / 10%); +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link, +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .model-name { +color: #f8f8f8; +text-transform: none; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link:hover, +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link:focus-visible { +background: transparent; +border-radius: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-item.selected .verb-link { +background: transparent; +border-radius: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .selected-icon { +flex-shrink: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .selected-icon svg { +width: 12px; +height: 12px; +display: block; +} + +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link { +font-size: 14px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .model-link { +font-size: 14px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-item .selected-icon { +display: none; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-item.selected .selected-icon { +display: block; +position: absolute; +top: 50%; +left: 3px; +transform: translateY(-50%); +} + +[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-item.selected .selected-icon { +right: 3px; +left: unset; +} + +@media (max-width: 1024px) { +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .verb-list { + animation: pbu-model-move-down 0.4s cubic-bezier(0.5, 1.8, 0.3, 0.8) forwards; +} +} + +@media screen and (max-width: 599px) { +.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container { + width: auto; +} + +[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .verb-list { + left: unset; +} + +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list { + top: unset; + left: unset; +} + +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .models-container .verb-list { + left: 0; + width: 100%; + box-sizing: border-box; + margin-top: 30px; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .model-name { + display: none; +} + +:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .models-container { + position: static; +} +} + +.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn { +background: transparent; +border: none; +padding: 7px 12px; +gap: 6px; +color: #f8f8f8; +} + +.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-ico { +display: flex; +align-items: center; +flex-shrink: 0; +} + +.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-ico svg { +width: 20px; +height: 20px; +} + +.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-txt { +color: #f8f8f8; +font-size: 15px; +font-weight: 400; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list, +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container .verb-list { +top: auto; +bottom: 100%; +transform: none; +animation: none; +margin-bottom: 4px; +z-index: 10; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container, +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container { +position: relative; +z-index: 1; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container.show-menu, +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container.show-menu { +z-index: 300; +} + +.pbu-drop-zone-wrap { +position: relative; +width: 123px; +height: 123px; +min-height: 123px; +flex-shrink: 0; +box-sizing: border-box; +} + +.pbu-drop-zone-wrap .drop-zone { +margin-top: 0px; +width: 123px; +height: 123px; +border-radius: 13.667px; +border: 2px dashed rgba(198, 198, 198, 0.50); +padding: 0; +background: none; +display: flex; +flex-direction: column; +align-items: center; +justify-content: center; +gap: 8px; +box-sizing: border-box; +} + +.pbu-drop-zone-wrap .drop-zone:hover, +.pbu-drop-zone-wrap .drop-zone.drag-over { +border-color: rgb(255 255 255 / 55%); +background: rgb(255 255 255 / 4%); +} + +.pbu-drop-zone-wrap .drop-zone.drag-over { +border-color: #4069FD; +border: 2px solid; +background: rgb(64 105 253 / 14%); +} + +.pbu-drop-zone-wrap .drop-zone.hidden { +display: none; +} + +.pbu-drop-content { +display: flex; +flex-direction: column; +align-items: center; +gap: 6px; +pointer-events: none; +} + +.pbu-upload-svg { +width: 28px; +height: 28px; +color: rgb(255 255 255 / 65%); +} + +.pbu-upload-text { +font-size: 13px; +line-height: 1.35; +color: rgb(255 255 255 / 70%); +} + +.pbu-legal-text { +font-size: 10px; +color: rgb(255 255 255 / 45%); +margin: 0; +} + +.pbu-preview { +position: absolute; +top: 0; +left: 0; +width: 123px; +height: 123px; +box-sizing: border-box; +border-radius: 13.667px; +overflow: hidden; +border: 2px solid #2680eb; +background: rgb(0 0 0 / 20%); +} + +.pbu-preview.hidden { +display: none; +} + +.pbu-preview-img { +display: block; +width: 100%; +height: 100%; +object-fit: cover; +border-radius: 13.667px; +border: 2px solid #4069FD; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-delete-btn { +border-radius: 16px; +background: rgb(255 255 255 / 94%); +box-shadow: 0 2px 8px 0 rgb(0 0 0 / 16%); +display: flex; +justify-content: center; +align-items: center; +position: absolute; +top: 50%; +left: 50%; +right: auto; +transform: translate(-50%, -50%); +width: 32px; +height: 32px; +padding: 0; +border: none; +cursor: pointer; +opacity: 0; +transition: opacity 0.2s; +z-index: 2; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-delete-btn svg { +display: block; +width: 18px; +height: 18px; +} + +.pbu-drop-zone-wrap .pbu-select-spinner { +position: absolute; +top: 0; +left: 0; +width: 123px; +height: 123px; +box-sizing: border-box; +border-radius: 13.667px; +display: flex; +align-items: center; +justify-content: center; +background: rgb(0 0 0 / 35%); +z-index: 3; +} + +.pbu-drop-zone-wrap.pbu-select-processing .drop-zone { +background: #000; +border: 2px solid #4069FD; +border-style: solid; +} + +.pbu-drop-zone-wrap.pbu-select-processing .pbu-drop-content { +visibility: hidden; +pointer-events: none; +} + +.pbu-drop-zone-wrap.pbu-select-processing .pbu-select-spinner { +background: transparent; +} + +.pbu-drop-zone-wrap .pbu-select-spinner.hidden { +display: none; +} + +.pbu-select-spinner-ring { +width: 32px; +height: 32px; +border: 3px solid rgb(255 255 255 / 35%); +border-top-color: #fff; +border-radius: 50%; +animation: pbu-spin 0.7s linear infinite; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-preview:hover .pbu-delete-btn, +.unity-prompt-bar-upload.unity-enabled .pbu-delete-btn:focus-visible { +opacity: 1; +} + +.pbu-spinner { +position: absolute; +inset: 0; +display: flex; +align-items: center; +justify-content: center; +background: rgb(0 0 0 / 35%); +} + +.pbu-spinner.hidden { +display: none; +} + +.pbu-spinner::after { +content: ''; +width: 32px; +height: 32px; +border: 3px solid rgb(255 255 255 / 35%); +border-top-color: #fff; +border-radius: 50%; +animation: pbu-spin 0.7s linear infinite; +} + +@keyframes pbu-spin { +to { transform: rotate(360deg); } +} + +.action-container > a.unity-act-btn.pbu-more-btn.more-btn { +display: flex; +gap: 6px; +text-decoration: none; +} + +.action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico { +height: 20px; +width: 20px;; +padding: 6px 0px 6px 10px; +align-self: center; +} + +.action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico svg { +width: 20px; +height: 20px; +} +.action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-txt { +color: #C6C6C6; +} + +.pbu-main .pbu-controls-footer .act-wrap a.gen-btn { +border-radius: 25px; +background: linear-gradient(90deg, #D73220 0%, #D92361 33%, #7155FA 100%); +border: none; +padding: 10px 20px 10px 18px; +gap: 8px; +text-decoration: none; +display: flex; +align-items: center; +} + +.pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-ico { +height: fit-content; +display: flex; +} + +.pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-ico img { +width: 22px; +height: 22px; +} + +.pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-txt{ +color: var(--color-white); +font-size: 16px; +font-weight: 700; +line-height: normal; +} + +@media screen and (max-width: 599px) { + .unity-prompt-bar-upload.unity-enabled .interactive-area { + width: 100%; + max-width: 100%; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-main { + flex-direction: column; + gap: 0; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-left-section { + width: 100%; + max-width: 100%; + border-right: none; + padding-right: 0; + padding-bottom: 16px; + margin-bottom: 0; + border-bottom: 1px solid rgb(255 255 255 / 12%); + } + + .unity-prompt-bar-upload.unity-enabled .pbu-drop-zone-wrap { + width: 100%; + height: 160px; + min-height: 160px; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-drop-zone-wrap .drop-zone { + width: 100%; + height: 160px; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-preview { + width: 100%; + height: 160px; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-drop-zone-wrap .pbu-select-spinner { + width: 100%; + height: 160px; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-right-section { + width: 100%; + padding-top: 12px; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer { + flex-direction: row; + align-items: center; + gap: 8px; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container { + flex: 0 0 auto; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap { + flex: 1; + justify-content: flex-end; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap .gen-btn { + flex: 1; + justify-content: center; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area .pbu-controls-footer .pbu-aspect-models { + display: none; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-more-btn .btn-txt { + display: none; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model:hover, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model:hover { + background: transparent; + padding: 4px 6px; + } +} diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js new file mode 100644 index 00000000..4389b5f9 --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -0,0 +1,607 @@ +/* eslint-disable no-await-in-loop */ + +import { createTag, getUnityLibs } from '../../../scripts/utils.js'; + +function placeholderText(root, iconClass) { + const icon = root.querySelector(`.${iconClass}`) || root.querySelector(`[class*="${iconClass}"]`); + if (!icon) return ''; + return (icon.closest('li')?.innerText || '').replace(/\s+/g, ' ').trim(); +} + +function labelForField(root, iconClass, fallback) { + return placeholderText(root, iconClass) || fallback; +} + +function extractLegalFootFromAuthoring(root) { + const marker = root.querySelector('[class*="icon-legal-terms"]'); + if (!marker) return null; + const li = marker.closest('li'); + const foot = createTag('div', { class: 'pbu-legal-foot' }); + if (li?.parentElement) { + while (li.firstChild) foot.append(li.firstChild); + li.remove(); + return foot; + } + foot.append(marker.cloneNode(true)); + marker.remove(); + return foot; +} + +function svgIcon(href) { + return ``; +} + +function syncDropdownSelection(list, activeLink) { + list.querySelectorAll('li').forEach((li) => { + const a = li.querySelector('a'); + const isActive = a === activeLink; + li.classList.toggle('selected', isActive); + a?.setAttribute('aria-selected', isActive ? 'true' : 'false'); + }); +} + +function closeDropdown(container, triggerBtn, list) { + container.classList.remove('show-menu'); + list.setAttribute('style', 'display: none;'); + triggerBtn.setAttribute('aria-expanded', 'false'); +} + +function buildDropdownShell({ label, menuId, extraClass = '', imgEl = null, ariaLabelledBy = null }) { + const container = createTag('div', { + class: `models-container${extraClass ? ` ${extraClass}` : ''}`, + 'aria-label': label, + }); + + const nameContainer = createTag('span', { class: 'model-name' }); + const menuIcon = createTag('span', { class: 'menu-icon' }, svgIcon('#unity-chevron-icon')); + + const triggerBtn = createTag('button', { + type: 'button', + class: 'selected-model', + 'aria-expanded': 'false', + 'aria-controls': menuId, + 'aria-haspopup': 'listbox', + role: 'combobox', + }); + if (imgEl) triggerBtn.append(imgEl, nameContainer, menuIcon); + else triggerBtn.append(nameContainer, menuIcon); + + const listAttrs = { class: 'verb-list', id: menuId, role: 'listbox' }; + if (ariaLabelledBy) listAttrs['aria-labelledby'] = ariaLabelledBy; + const list = createTag('ul', listAttrs); + list.setAttribute('style', 'display: none;'); + + container.append(triggerBtn, list); + return { + container, triggerBtn, nameContainer, menuIcon, list, + }; +} + +function attachDropdownBehavior(container, triggerBtn, list) { + const getOptions = () => [...list.querySelectorAll('a.model-link')]; + const focusSelectedOrFirst = () => { + const options = getOptions(); + if (!options.length) return; + const selected = options.find((option) => option.getAttribute('aria-selected') === 'true'); + (selected || options[0])?.focus(); + }; + + triggerBtn.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.models-container.show-menu').forEach((other) => { + if (other === container) return; + other.classList.remove('show-menu'); + other.querySelector(':scope > .verb-list')?.setAttribute('style', 'display: none;'); + other.querySelector('.selected-model')?.setAttribute('aria-expanded', 'false'); + }); + const isOpen = container.classList.toggle('show-menu'); + if (isOpen) list.removeAttribute('style'); + else list.setAttribute('style', 'display: none;'); + triggerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + }); + + triggerBtn.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + closeDropdown(container, triggerBtn, list); + triggerBtn.focus(); + return; + } + + if (!['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(e.key)) return; + e.preventDefault(); + const isOpen = container.classList.contains('show-menu'); + if (!isOpen) { + container.classList.add('show-menu'); + list.removeAttribute('style'); + triggerBtn.setAttribute('aria-expanded', 'true'); + } + focusSelectedOrFirst(); + }); + + list.addEventListener('keydown', (e) => { + const options = getOptions(); + if (!options.length) return; + const idx = options.findIndex((option) => option === document.activeElement); + if (e.key === 'Tab') { + if (idx < 0) return; + const atStart = idx === 0; + const atEnd = idx === options.length - 1; + if ((e.shiftKey && atStart) || (!e.shiftKey && atEnd)) { + closeDropdown(container, triggerBtn, list); + } + return; + } + if (e.key === 'Escape') { + e.preventDefault(); + closeDropdown(container, triggerBtn, list); + triggerBtn.focus(); + return; + } + if (e.key === 'ArrowDown') { + e.preventDefault(); + const next = idx < 0 ? 0 : (idx + 1) % options.length; + options[next]?.focus(); + return; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + const next = idx < 0 ? options.length - 1 : (idx - 1 + options.length) % options.length; + options[next]?.focus(); + return; + } + if (e.key === 'Home') { + e.preventDefault(); + options[0]?.focus(); + return; + } + if (e.key === 'End') { + e.preventDefault(); + options[options.length - 1]?.focus(); + return; + } + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const active = idx >= 0 ? options[idx] : options[0]; + active?.click(); + } + }); + + document.addEventListener('click', (e) => { + if (!container.contains(/** @type {Node} */ (e.target))) { + closeDropdown(container, triggerBtn, list); + } + }); +} + +export default class PromptBarUploadWidget { + constructor(target, el, workflowCfg, spriteCon) { + this.target = target; + this.el = el; + this.workflowCfg = workflowCfg; + this.spriteCon = spriteCon; + this.widgetWrap = null; + this.actionMap = {}; + this.models = null; + this.aspectRatioMap = {}; + this.sizeMap = {}; + this.selectedModelId = ''; + this.selectedAspectRatio = ''; + this.lanaOptions = { sampleRate: 1, tags: 'Unity-FF-PBU' }; + this.showAspectRatio = false; + this.showMore = false; + this.actionContainerEl = null; + } + + async loadModels() { + const { origin } = window.location; + const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) + ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` + : origin; + const res = await fetch(`${baseUrl}/unity/configs/prompt/model-picker-video.json`); + if (!res.ok) throw new Error('Failed to fetch video models.'); + const json = await res.json(); + this.models = json?.content?.data || []; + this.buildAspectRatioMap(); + } + + buildAspectRatioMap() { + this.aspectRatioMap = {}; + this.sizeMap = {}; + const parseList = (str) => { + const s = String(str); + try { + const parsed = JSON.parse(s); + if (Array.isArray(parsed)) return parsed.map(String); + } catch { /* fall through to comma-split */ } + return s.split(',').map((v) => v.trim()).filter(Boolean); + }; + (this.models || []).forEach((item) => { + const raw = item['aspect-ratio']; + if (!item.id || !raw) return; + const ratios = parseList(raw); + this.aspectRatioMap[item.id] = ratios; + const widths = item.width != null ? parseList(item.width) : []; + const heights = item.height != null ? parseList(item.height) : []; + this.sizeMap[item.id] = ratios.map((_, i) => ({ + width: Number(widths[i]) || null, + height: Number(heights[i]) || null, + })); + }); + } + + getAspectRatiosForModel(modelId) { + return this.aspectRatioMap[modelId] || []; + } + + getSizeForAspectRatio(modelId, ratio) { + const sizes = this.sizeMap[modelId] || []; + const ratios = this.aspectRatioMap[modelId] || []; + const idx = ratios.indexOf(ratio); + return idx !== -1 ? sizes[idx] : null; + } + + readFeatureFlags() { + this.showAspectRatio = !!this.el.querySelector('[class*="icon-show-aspect-ratio"]'); + this.showMore = !!this.el.querySelector('[class*="icon-show-more"]'); + } + + buildModelPicker() { + if (!this.models?.length) return null; + const defaultModel = this.models.find((m) => m.default === 'true' || m.default === true) || this.models[0]; + this.selectedModelId = defaultModel?.id || ''; + + const imgEl = defaultModel?.icon ? createTag('img', { src: defaultModel.icon, alt: '' }) : null; + const { container, triggerBtn, nameContainer, list } = buildDropdownShell({ + label: 'Model options', + menuId: 'pbu-model-menu', + imgEl, + ariaLabelledBy: 'listbox-label', + }); + nameContainer.textContent = (defaultModel?.name || '').trim(); + + this.models.forEach((model, idx) => { + const selectedIcon = createTag('span', { class: 'selected-icon' }, svgIcon('#unity-checkmark-icon')); + const nameSpan = createTag('span', { class: 'model-name' }, (model.name || model.id || '').trim()); + const link = createTag('a', { + href: '#', + class: 'verb-link model-link', + 'data-model-id': model.id, + 'data-model-name': (model.name || '').trim(), + 'data-model-icon': model.icon || '', + ...(model.version != null && model.version !== '' ? { 'data-model-version': String(model.version) } : {}), + 'aria-selected': idx === 0 ? 'true' : 'false', + role: 'option', + }); + link.append(selectedIcon); + if (model.icon) link.append(createTag('img', { src: model.icon, alt: '' })); + link.append(nameSpan); + const li = createTag('li', { class: `verb-item${idx === 0 ? ' selected' : ''}`, role: 'presentation' }); + li.append(link); + list.append(li); + }); + + list.addEventListener('click', (e) => { + const link = e.target.closest('a.model-link'); + if (!link) return; + e.preventDefault(); + e.stopPropagation(); + const modelId = link.getAttribute('data-model-id') || ''; + const modelName = link.getAttribute('data-model-name') || ''; + const modelIcon = link.getAttribute('data-model-icon') || ''; + const modelVersion = link.getAttribute('data-model-version') || ''; + this.selectedModelId = modelId; + nameContainer.textContent = modelName; + const triggerIcon = triggerBtn.querySelector(':scope > img'); + if (modelIcon) { + if (triggerIcon) { + triggerIcon.setAttribute('src', modelIcon); + } else { + triggerBtn.prepend(createTag('img', { src: modelIcon, alt: '' })); + } + } else if (triggerIcon) { + triggerIcon.remove(); + } + this.widgetWrap?.setAttribute('data-selected-model-id', modelId); + this.widgetWrap?.setAttribute('data-selected-model-name', modelName); + if (modelVersion) this.widgetWrap?.setAttribute('data-selected-model-version', modelVersion); + else this.widgetWrap?.removeAttribute('data-selected-model-version'); + syncDropdownSelection(list, link); + closeDropdown(container, triggerBtn, list); + if (this.showAspectRatio) this.updateAspectRatioOptions(modelId); + }); + + triggerBtn.addEventListener('click', () => triggerBtn.dispatchEvent(new CustomEvent('pbu-model-dropdown-open', { bubbles: true }))); + attachDropdownBehavior(container, triggerBtn, list); + this.widgetWrap?.setAttribute('data-selected-model-id', this.selectedModelId); + this.widgetWrap?.setAttribute('data-selected-model-name', (defaultModel?.name || '').trim()); + if (defaultModel?.version != null && defaultModel.version !== '') { + this.widgetWrap?.setAttribute('data-selected-model-version', String(defaultModel.version)); + } else { + this.widgetWrap?.removeAttribute('data-selected-model-version'); + } + return container; + } + + setSelectedAspectRatio(modelId, ratio) { + this.selectedAspectRatio = ratio; + this.widgetWrap?.setAttribute('data-selected-aspect-ratio', ratio); + const size = this.getSizeForAspectRatio(modelId, ratio); + if (size?.width) this.widgetWrap?.setAttribute('data-selected-width', size.width); + else this.widgetWrap?.removeAttribute('data-selected-width'); + if (size?.height) this.widgetWrap?.setAttribute('data-selected-height', size.height); + else this.widgetWrap?.removeAttribute('data-selected-height'); + } + + syncDefaultAttributes() { + if (!this.widgetWrap || !this.selectedModelId) return; + const defaultModel = this.models?.find((m) => m.id === this.selectedModelId); + this.widgetWrap.setAttribute('data-selected-model-id', this.selectedModelId); + this.widgetWrap.setAttribute('data-selected-model-name', (defaultModel?.name || '').trim()); + if (defaultModel?.version != null && defaultModel.version !== '') { + this.widgetWrap.setAttribute('data-selected-model-version', String(defaultModel.version)); + } else { + this.widgetWrap.removeAttribute('data-selected-model-version'); + } + if (this.selectedAspectRatio) { + this.setSelectedAspectRatio(this.selectedModelId, this.selectedAspectRatio); + } + } + + buildAspectRatioDropdown(modelId) { + const ratios = this.getAspectRatiosForModel(modelId); + if (!ratios.length) return null; + this.setSelectedAspectRatio(modelId, ratios[0]); + + const { container, triggerBtn, nameContainer, list } = buildDropdownShell({ + label: 'Aspect ratio', + menuId: 'pbu-aspect-menu', + extraClass: 'pbu-aspect-models', + }); + nameContainer.textContent = ratios[0]; + + ratios.forEach((ratio, idx) => { + const selectedIcon = createTag('span', { class: 'selected-icon' }, svgIcon('#unity-checkmark-icon')); + const link = createTag('a', { + href: '#', + class: 'verb-link model-link', + 'data-ratio': ratio, + 'aria-selected': idx === 0 ? 'true' : 'false', + role: 'option', + }); + link.append(selectedIcon, createTag('span', { class: 'model-name' }, ratio)); + const li = createTag('li', { class: `verb-item${idx === 0 ? ' selected' : ''}`, role: 'presentation' }); + li.append(link); + list.append(li); + }); + + list.addEventListener('click', (e) => { + const link = e.target.closest('a.model-link'); + if (!link) return; + e.preventDefault(); + e.stopPropagation(); + const ratio = link.getAttribute('data-ratio') || ''; + nameContainer.textContent = ratio; + this.setSelectedAspectRatio(modelId, ratio); + syncDropdownSelection(list, link); + closeDropdown(container, triggerBtn, list); + }); + + triggerBtn.addEventListener('click', () => triggerBtn.dispatchEvent(new CustomEvent('pbu-ratio-dropdown-open', { bubbles: true }))); + attachDropdownBehavior(container, triggerBtn, list); + return container; + } + + updateAspectRatioOptions(modelId) { + const ac = this.actionContainerEl ?? this.widgetWrap?.querySelector('.action-container'); + ac?.querySelector('.pbu-aspect-models')?.remove(); + const picker = this.buildAspectRatioDropdown(modelId); + if (!picker || !ac) return; + const modelPicker = ac.querySelector('.models-container:not(.pbu-aspect-models)'); + if (modelPicker) modelPicker.after(picker); + else ac.append(picker); + } + + buildLeftSection() { + const leftSectionLabel = placeholderText(this.el, 'icon-dropzone-label'); + const uploadLabel = createTag('div', { class: 'unity-slf-copy-label pbu-upload-heading' }, leftSectionLabel); + const { wrap: dropZoneWrap, ...dropZoneRefs } = this.buildDropZone(); + const leftSection = createTag('div', { class: 'pbu-left-section' }); + leftSection.append(uploadLabel, dropZoneWrap); + return { leftSection, dropZoneRefs }; + } + + buildRightSection() { + const promptHeading = placeholderText(this.el, 'icon-placeholder-prompt') + || labelForField(this.el, 'icon-label-prompt', 'Prompt'); + const promptLabel = createTag('label', { + for: 'pbuPromptInput', + class: 'unity-slf-copy-label unity-slf-prompt-label', + }, promptHeading); + + const promptTextarea = this.buildPromptTextarea(); + + const actionContainer = createTag('div', { class: 'action-container' }); + this.actionContainerEl = actionContainer; + + if (this.models?.length) { + const mp = this.buildModelPicker(); + if (mp) actionContainer.append(mp); + } + if (this.showAspectRatio && this.selectedModelId) { + const ar = this.buildAspectRatioDropdown(this.selectedModelId); + if (ar) actionContainer.append(ar); + } + if (this.showMore) { + const moreBtn = this.buildMoreButton(); + if (moreBtn) actionContainer.append(moreBtn); + } + + const actWrap = createTag('div', { class: 'act-wrap' }); + actWrap.append(this.buildGenerateButton()); + + const controlsFooter = createTag('div', { class: 'pbu-controls-footer' }); + controlsFooter.append(actionContainer, actWrap); + + const promptBarContainer = createTag('div', { class: 'pbu-prompt-bar-container' }); + promptBarContainer.append(promptLabel, promptTextarea, controlsFooter); + + const rightSection = createTag('div', { class: 'pbu-right-section' }); + rightSection.append(promptBarContainer); + return rightSection; + } + + buildDropZone() { + const allowedFileTypes = this.workflowCfg?.targetCfg?.limits?.allowedFileTypes; + const fileInput = createTag('input', { + type: 'file', + id: 'file-upload', + accept: allowedFileTypes.join(','), + hidden: '', + 'aria-hidden': 'true', + }); + + const dropContent = createTag('div', { class: 'pbu-drop-content' }); + dropContent.append(createTag('img', { loading: 'lazy', src: `${getUnityLibs()}/img/icons/upload.svg` })); + const dropZone = createTag('div', { + class: 'drop-zone', + role: 'button', + tabindex: '0', + 'aria-label': 'Upload image', + }); + dropZone.append(fileInput, dropContent); + dropZone.addEventListener('keydown', (e) => { + if (e.key !== 'Enter' && e.key !== ' ') return; + e.preventDefault(); + fileInput.click(); + }); + const selectSpinner = createTag('div', { class: 'pbu-select-spinner hidden', 'aria-hidden': 'true', role: 'status' }); + selectSpinner.append(createTag('div', { class: 'pbu-select-spinner-ring' })); + + const previewImg = createTag('img', { class: 'pbu-preview-img', alt: 'Selected image preview' }); + const deleteBtn = createTag('button', { type: 'button', class: 'pbu-delete-btn', 'aria-label': 'Remove image' }); + deleteBtn.innerHTML = svgIcon('#unity-trash-icon'); + const uploadSpinner = createTag('div', { class: 'pbu-spinner hidden', 'aria-label': 'Uploading', role: 'status' }); + const preview = createTag('div', { class: 'pbu-preview hidden', 'aria-hidden': 'true' }); + preview.append(previewImg, deleteBtn, uploadSpinner); + + const wrap = createTag('div', { class: 'pbu-drop-zone-wrap' }); + wrap.append(dropZone, selectSpinner, preview); + return { wrap, dropZone, preview, previewImg, deleteBtn,}; + } + + buildPromptTextarea() { + const defaultPrompt = placeholderText(this.el, 'icon-default-prompt') || ''; + const maxCharLimit = this.workflowCfg?.targetCfg?.limits?.['max-char-limit'] ?? 750; + const textarea = createTag('textarea', { + id: 'pbuPromptInput', + class: 'inp-field', + rows: '1', + maxlength: String(maxCharLimit), + 'aria-label': defaultPrompt, + 'aria-autocomplete': 'list', + }); + textarea.value = defaultPrompt; + textarea.addEventListener('input', () => textarea.dispatchEvent(new CustomEvent('pbu-enter-prompt', { bubbles: true })), { once: true }); + return textarea; + } + + buildGenerateButton() { + const generateLi = this.el.querySelector('[class*="icon-generate"]')?.closest('li'); + const genBtnText = (generateLi?.innerText).trim().split('\n')[0] || 'Generate'; + const img = generateLi?.querySelector('img[src*=".svg"]'); + const btn = createTag('a', { href: '#', class: 'unity-act-btn gen-btn', 'daa-ll': 'Generate-video', 'aria-label': genBtnText }); + if (img) { + img.setAttribute('alt', 'Generate video'); + btn.append(createTag('div', { class: 'btn-ico' }, img)); + } + if (genBtnText) btn.append(createTag('div', { class: 'btn-txt' }, genBtnText.split('\n')[0])); + return btn; + } + + buildMoreButton() { + if (!this.showMore) return null; + const moreLi = this.el.querySelector('[class*="icon-more"]')?.closest('li'); + const txt = (moreLi?.innerText || 'More').trim().split('\n')[0] || 'More'; + const btn = createTag('a', { href: '#', class: 'unity-act-btn pbu-more-btn more-btn', 'aria-label': txt }); + btn.append( + createTag('span', { class: 'btn-ico' }, svgIcon('#unity-more-icon')), + createTag('div', { class: 'btn-txt' }, txt), + ); + btn.addEventListener('click', () => btn.dispatchEvent(new CustomEvent('pbu-more-click', { bubbles: true }))); + return btn; + } + + addWidget() { + const interactArea = this.target?.querySelector('.copy'); + const { target: anchorSelector, insert } = this.workflowCfg.targetCfg || {}; + const para = anchorSelector ? interactArea?.querySelector(anchorSelector) : null; + if (para && insert === 'before') para.before(this.widgetWrap); + else if (para) para.after(this.widgetWrap); + else interactArea?.appendChild(this.widgetWrap); + } + + + wireImagePreview({ dropZone, preview, previewImg, deleteBtn }) { + const showPreview = (file) => { + const url = URL.createObjectURL(file); + previewImg.src = url; + previewImg.onload = () => URL.revokeObjectURL(url); + dropZone.classList.add('hidden'); + dropZone.setAttribute('aria-hidden', 'true'); + preview.classList.remove('hidden'); + preview.removeAttribute('aria-hidden'); + }; + + const showDropZone = () => { + dropZone.classList.remove('hidden'); + dropZone.removeAttribute('aria-hidden'); + preview.classList.add('hidden'); + preview.setAttribute('aria-hidden', 'true'); + previewImg.src = ''; + }; + + this.widgetWrap?.addEventListener('pbu-image-selected', (e) => showPreview(e.detail.file)); + deleteBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + showDropZone(); + this.widgetWrap?.dispatchEvent(new CustomEvent('pbu-delete-image')); + }); + } + + async initWidget() { + this.readFeatureFlags(); + + try { + await this.loadModels(); + } catch (e) { + window.lana?.log(`Message: Failed to load video models, Error: ${e}`, this.lanaOptions); + } + + const { leftSection, dropZoneRefs } = this.buildLeftSection(); + const rightSection = this.buildRightSection(); + const main = createTag('div', { class: 'pbu-main' }); + main.append(leftSection, rightSection); + const skin = this.el.classList.contains('light') ? 'light' : 'dark'; + const interactiveShell = createTag('div', { class: `interactive-area ${skin}` }); + interactiveShell.append(main); + const root = createTag('div', { class: 'unity-prompt-bar-upload unity-enabled' }); + root.append(interactiveShell); + const holder = createTag('div', { class: 'unity-pbu-config-holder unity-slf-sr-only' }); + holder.setAttribute('aria-hidden', 'true'); + while (this.el.firstChild) holder.append(this.el.firstChild); + this.el.append(holder); + this.el.classList.add('unity-prompt-bar-upload-host'); + const unitySprite = createTag('div', { class: 'unity-sprite-container' }); + unitySprite.innerHTML = this.spriteCon || ''; + const legalFoot = extractLegalFootFromAuthoring(this.el); + this.widgetWrap = createTag('div', { class: 'ex-unity-wrap verb-options pbu-widget' }); + this.widgetWrap.append(unitySprite, root); + if (legalFoot) this.widgetWrap.append(legalFoot); + this.syncDefaultAttributes(); + + this.addWidget(); + this.wireImagePreview(dropZoneRefs); + return this.workflowCfg.targetCfg.actionMap; + } +} diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js new file mode 100644 index 00000000..3b43cbce --- /dev/null +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js @@ -0,0 +1,733 @@ +/* eslint-disable max-len */ +/* eslint-disable max-classes-per-file */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable class-methods-use-this */ +/* eslint-disable no-restricted-syntax */ + +import { + unityConfig, + getUnityLibs, + priorityLoad, + createTag, + getLocale, + getLibs, + getHeaders, + getApiCallOptions, + sendAnalyticsEvent, +} from '../../../scripts/utils.js'; + +function normalizeToArray(value) { + if (value == null) return []; + if (Array.isArray(value)) return value.filter(Boolean); + if (typeof value.forEach === 'function' && typeof value.length === 'number') { + try { return [...value]; } catch { return [value]; } + } + return [value]; +} + +class ServiceHandler { + constructor(renderWidget = false, canvasArea = null, unityEl = null, workflowCfg = {}, getAdditionalHeaders = null) { + this.renderWidget = renderWidget; + this.canvasArea = canvasArea; + this.unityEl = unityEl; + this.workflowCfg = workflowCfg; + this.getAdditionalHeaders = getAdditionalHeaders; + } + + async postCallToService(api, options, failOnError = true) { + const postOpts = { + method: 'POST', + headers: await getHeaders(unityConfig.apiKey, this.getAdditionalHeaders?.() || {}), + ...options, + }; + let response; + try { + response = await fetch(api, postOpts); + } catch (e) { + if (e instanceof TypeError) { + const error = new Error(`Network error. URL: ${api}; Error message: ${e.message}`); + error.status = 0; + throw error; + } + throw e; + } + if (failOnError && response.status !== 200) { + const error = new Error('Operation failed'); + error.status = response.status; + throw error; + } + if (!failOnError) return response; + return response.json(); + } + + showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') { + sendAnalyticsEvent(new CustomEvent(`Upload ${errorType} error|UnityWidget|${errorCallbackOptions.errorCode || ''}|${JSON.stringify(errorCallbackOptions.fileMetaData) || ''}`)); + if (!errorCallbackOptions.errorToastEl) return; + const msg = this.unityEl.querySelector(errorCallbackOptions.errorType)?.closest('li')?.textContent?.trim(); + this.canvasArea.forEach((element) => { + element.style.pointerEvents = 'none'; + const errorToast = element.querySelector('.alert-holder'); + if (!errorToast) return; + const closeBtn = errorToast.querySelector('.alert-close'); + if (closeBtn) closeBtn.style.pointerEvents = 'auto'; + const alertText = errorToast.querySelector('.alert-text p'); + if (!alertText) return; + alertText.innerText = msg; + errorToast.classList.add('show'); + }); + window.lana?.log(`Message: ${msg}, Error: ${error || ''}`, lanaOptions); + } +} + +export default class ActionBinder { + constructor(unityEl, workflowCfg, block, canvasArea, actionMap = {}) { + this.unityEl = unityEl; + this.workflowCfg = workflowCfg; + this.block = block; + this.canvasArea = canvasArea; + this.actionMap = actionMap; + this.errorToastEl = null; + this.transitionScreen = null; + this.LOADER_LIMIT = 95; + this.serviceHandler = null; + this.uploadAbortController = null; + this.assetId = null; + this.pendingFile = null; + this.filesData = {}; + this.sendAnalyticsToSplunk = null; + this.analyticsModule = null; + this.promiseStack = []; + this.desktop = false; + this.toastCanvasAreas = normalizeToArray(canvasArea); + this.apiConfig = this.getApiConfig(); + this.verb = this.getVerbFromDom(); + this.initActionListeners = this.initActionListeners.bind(this); + const searchRoot = canvasArea || block; + this.widgetWrap = searchRoot?.querySelector?.('.ex-unity-wrap') ?? searchRoot; + this.inputField = searchRoot?.querySelector?.('.inp-field'); + this.limits = workflowCfg.targetCfg?.limits || {}; + const productTag = workflowCfg.targetCfg?.[`productTag-${workflowCfg.productName?.toLowerCase()}`] || 'FF'; + this.lanaOptions = { sampleRate: 1, tags: `Unity-${productTag}-PBU` }; + + } + + getApiConfig() { + unityConfig.endPoint = { + assetUpload: `${unityConfig.apiEndPoint}/asset`, + acmpCheck: `${unityConfig.apiEndPoint}/asset/finalize`, + }; + return unityConfig; + } + + getAdditionalHeaders() { + const baseAction = this.workflowCfg?.supportedFeatures?.values()?.next()?.value; + const xUnityAction = this.verb ? `${baseAction}-${this.verb}` : baseAction; + return { + 'x-unity-product': this.workflowCfg?.productName, + 'x-unity-action': xUnityAction, + }; + } + + getVerbFromDom() { + const verbEl = this.unityEl?.querySelector('[class*="icon-operation-"]'); + if (verbEl) { + const verbClass = Array.from(verbEl.classList).find((cls) => cls.startsWith('icon-operation-')); + const fromDom = verbClass?.slice('icon-operation-'.length); + if (fromDom) return fromDom; + } + return this.workflowCfg?.enabledFeatures?.[0]; + } + + async initAnalytics() { + if (this.analyticsModule) return; + this.analyticsModule = await import(`${getUnityLibs()}/scripts/analytics.js`); + if (this.workflowCfg.targetCfg?.sendSplunkAnalytics) { + this.sendAnalyticsToSplunk = this.analyticsModule.default; + } + } + + logAnalytics(eventName, data) { + this.sendAnalyticsToSplunk?.( + eventName, + this.workflowCfg.productName, + { ...data, operation: this.verb }, + `${unityConfig.apiEndPoint}/log`, + true, + ); + } + + resetUploadedAssetState({ dropPendingImage = false } = {}) { + this.uploadAbortController?.abort(); + this.uploadAbortController = null; + this.assetId = null; + if (dropPendingImage) { + this.pendingFile = null; + this.filesData = {}; + } + } + + async createErrorToast() { + try { + const [alertImg, closeImg] = await Promise.all([ + fetch(`${getUnityLibs()}/img/icons/alert.svg`).then((res) => res.text()), + fetch(`${getUnityLibs()}/img/icons/close.svg`).then((res) => res.text()), + ]); + const { decorateDefaultLinkAnalytics } = await import(`${getLibs()}/martech/attributes.js`); + this.toastCanvasAreas.forEach((canvasEl) => { + const mount = canvasEl.querySelector('.pbu-main') || canvasEl; + const alertText = createTag('div', { class: 'alert-text' }, createTag('p', {}, 'Alert Text')); + const alertIcon = createTag('div', { class: 'alert-icon' }); + alertIcon.innerHTML = alertImg; + alertIcon.append(alertText); + const alertClose = createTag('a', { class: 'alert-close', href: '#' }); + alertClose.innerHTML = closeImg; + alertClose.append(createTag('span', { class: 'alert-close-text' }, 'Close error toast')); + const alertContent = createTag('div', { class: 'alert-content' }); + alertContent.append(alertIcon, alertClose); + const alertToast = createTag('div', { class: 'alert-toast' }, alertContent); + const errholder = createTag('div', { class: 'alert-holder' }, alertToast); + alertClose.addEventListener('click', (e) => { + this.preventDefault(e); + errholder.classList.remove('show'); + canvasEl.style.pointerEvents = 'auto'; + }); + decorateDefaultLinkAnalytics(errholder); + mount.append(errholder); + }); + return this.toastCanvasAreas[0]?.querySelector('.pbu-main .alert-holder') + || this.toastCanvasAreas[0]?.querySelector('.alert-holder'); + } catch (e) { + window.lana?.log(`Message: Error creating error toast, Error: ${e}`, this.lanaOptions); + return null; + } + } + + extractFiles(e) { + const files = []; + if (e.dataTransfer?.items) { + [...e.dataTransfer.items].forEach((item) => { if (item.kind === 'file') files.push(item.getAsFile()); }); + } else if (e.target?.files) { + [...e.target.files].forEach((file) => files.push(file)); + } + return files; + } + + handleClientError(errorTypeSelector, errorCode, message = '') { + this.serviceHandler.showErrorToast( + { + errorToastEl: this.errorToastEl, + errorType: errorTypeSelector, + errorCode, + fileMetaData: this.filesData, + }, + message, + this.lanaOptions, + 'client', + ); + this.logAnalytics('Upload client error|UnityWidget', { errorData: { code: errorCode }, fileMetaData: this.filesData }); + } + + setSelectSpinnerVisible(visible) { + const wrap = this.widgetWrap?.querySelector('.pbu-drop-zone-wrap'); + const el = wrap?.querySelector('.pbu-select-spinner'); + if (!el) return; + el.classList.toggle('hidden', !visible); + el.setAttribute('aria-hidden', visible ? 'false' : 'true'); + wrap?.classList.toggle('pbu-select-processing', !!visible); + } + + async validateAndStoreFile(files) { + this.setSelectSpinnerVisible(true); + try { + if (!files?.length) return false; + if (files.length > (this.limits.maxNumFiles || 1)) { + this.handleClientError('.icon-error-filecount', 'error-filecount'); + return false; + } + const file = files[0]; + this.filesData = { count: files.length, size: file.size, type: file.type }; + if (this.limits.allowedFileTypes && !this.limits.allowedFileTypes.includes(file.type)) { + this.handleClientError('.icon-error-filetype', 'error-filetype'); + return false; + } + if (this.limits.maxFileSize && file.size > this.limits.maxFileSize) { + this.handleClientError('.icon-error-filesize', 'error-filesize'); + return false; + } + this.resetUploadedAssetState(); + this.pendingFile = file; + this.widgetWrap?.dispatchEvent(new CustomEvent('pbu-image-selected', { detail: { file } })); + return true; + } finally { + this.setSelectSpinnerVisible(false); + } + } + + async uploadImgToUnity(storageUrl, _id, blobData, fileType, signal) { + const uploadOptions = { + method: 'PUT', + headers: { 'Content-Type': fileType }, + body: blobData, + ...(signal && { signal }), + }; + let response; + try { + response = await fetch(storageUrl, uploadOptions); + } catch (e) { + if (e instanceof TypeError) { + const error = new Error(`Network error. URL: ${storageUrl}; Error message: ${e.message}`); + error.status = 0; + throw error; + } + throw e; + } + if (response.status !== 200) { + const error = new Error('Failed to upload image to Unity'); + error.status = response.status; + throw error; + } + } + + async uploadAsset(file) { + const assetDetails = { + targetProduct: this.workflowCfg.productName, + name: file.name, + size: file.size, + format: file.type, + }; + this.uploadAbortController = new AbortController(); + const { signal } = this.uploadAbortController; + try { + const resJson = await this.serviceHandler.postCallToService( + this.apiConfig.endPoint.assetUpload, + { body: JSON.stringify(assetDetails) }, + ); + const { id, href, blocksize, uploadUrls } = resJson; + this.assetId = id; + this.logAnalytics('Asset Created|UnityWidget', { assetId: this.assetId }); + const { default: UploadHandler } = await import(`${getUnityLibs()}/core/workflow/workflow-upload/upload-handler.js`); + const uploadHandler = new UploadHandler(this, this.serviceHandler); + if (blocksize && uploadUrls && Array.isArray(uploadUrls)) { + const { failedChunks, attemptMap } = await uploadHandler.uploadChunksToUnity(uploadUrls, file, blocksize, signal); + if (failedChunks?.size > 0) { + if (signal.aborted) return false; + const error = new Error(`One or more chunks failed for asset: ${id}`); + error.status = 504; + this.logAnalytics('Chunked Upload Failed|UnityWidget', { + assetId: this.assetId, + failedChunks: failedChunks.size, + maxRetryCount: Math.max(...Array.from(attemptMap.values())), + }); + throw error; + } + await uploadHandler.scanImgForSafetyWithRetry(this.assetId, signal); + const { createChunkAnalyticsData } = await import(`${getUnityLibs()}/utils/chunkingUtils.js`); + const totalChunks = Math.ceil(file.size / blocksize); + this.logAnalytics( + 'Chunked Upload Completed|UnityWidget', + createChunkAnalyticsData('Chunked Upload Completed|UnityWidget', { + assetId: this.assetId, + chunkCount: totalChunks, + totalFileSize: file.size, + fileType: file.type, + }), + ); + } else { + await this.uploadImgToUnity(href, id, file, file.type, signal); + await uploadHandler.scanImgForSafetyWithRetry(this.assetId, signal); + this.logAnalytics('Upload Completed|UnityWidget', { assetId: this.assetId }); + } + return true; + } catch (e) { + if (signal.aborted || e.name === 'AbortError') { + window.lana?.log(`Message: Upload aborted, Error: ${e.message}`, this.lanaOptions); + return false; + } + this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, e, this.lanaOptions); + this.logAnalytics('Upload server error|UnityWidget', { + errorData: { code: 'error-request', subCode: `uploadAsset ${e.status}`, desc: e.message }, + assetId: this.assetId, + }); + return false; + } + } + + validateInput(query) { + const maxCharLimit = this.limits?.['max-char-limit'] ?? 750; + if (query.length > maxCharLimit) { + this.handleClientError('.icon-error-max-length', 'max-prompt-characters-exceeded', 'Prompt too long'); + this.logAnalytics('generate', { errorData: { code: 'max-prompt-characters-exceeded' } }); + return false; + } + return true; + } + + async ensureTransitionScreen() { + if (!this.transitionScreen) { + const { default: TransitionScreen } = await import(`${getUnityLibs()}/scripts/transition-screen.js`); + this.transitionScreen = new TransitionScreen(null, this.initActionListeners, this.LOADER_LIMIT, this.workflowCfg, this.desktop); + } + if (!this.transitionScreen.splashScreenEl) { + await this.transitionScreen.loadSplashFragment(); + } + } + + async handleGenerate(connectorGenerate = true) { + this.promiseStack = []; + if (!this.analyticsModule) await this.initAnalytics(); + const pbuEvents = this.analyticsModule.PROMPT_BAR_EVENTS; + const query = this.inputField?.value?.trim() || ''; + if (!this.validateInput(query)) return; + + const selectedModelId = this.widgetWrap?.getAttribute('data-selected-model-id') || ''; + const selectedAspectRatio = this.widgetWrap?.getAttribute('data-selected-aspect-ratio') || ''; + const selectedModelName = this.widgetWrap?.getAttribute('data-selected-model-name') || selectedModelId; + const ctaEventName = connectorGenerate ? pbuEvents.GENERATE_CTA : pbuEvents.MORE; + sendAnalyticsEvent(new CustomEvent(pbuEvents.UPLOAD_STARTED)); + sendAnalyticsEvent(new CustomEvent(ctaEventName)); + if (selectedModelName) sendAnalyticsEvent(new CustomEvent(pbuEvents.generateModel(selectedModelName))); + if (selectedAspectRatio) sendAnalyticsEvent(new CustomEvent(pbuEvents.ratioSelect(selectedAspectRatio))); + this.logAnalytics(pbuEvents.UPLOAD_STARTED, { fileMetaData: this.filesData }); + this.logAnalytics(ctaEventName, { + ...(selectedModelName && { + modelGenEventName: pbuEvents.generateModel(selectedModelName), + }), + assetId: this.assetId, + aspectRatio: selectedAspectRatio, + hasImage: !!this.pendingFile, + }); + const searchRoot = this.canvasArea || this.block; + const interactiveShell = searchRoot?.querySelector?.('.interactive-area'); + this.workflowCfg.theme = interactiveShell?.classList.contains('dark') ? 'dark' : null; + + await this.ensureTransitionScreen(); + await this.transitionScreen.showSplashScreen(true); + + if (this.pendingFile && !this.assetId) { + const uploadOk = await this.uploadAsset(this.pendingFile); + if (!uploadOk) { + await this.transitionScreen.showSplashScreen(); + return; + } + } + await this.continueInApp(query, selectedModelId, selectedAspectRatio, connectorGenerate); + } + + + async continueInApp(query, modelId, aspectRatio, connectorGenerate = true) { + const { getCgenQueryParams } = await import(`${getUnityLibs()}/utils/cgen-utils.js`); + const queryParams = getCgenQueryParams(this.unityEl); + const modelVersion = this.widgetWrap?.getAttribute('data-selected-model-version') || ''; + const selectedWidth = Number(this.widgetWrap?.getAttribute('data-selected-width')) || null; + const selectedHeight = Number(this.widgetWrap?.getAttribute('data-selected-height')) || null; + const size = (selectedWidth && selectedHeight) ? { width: selectedWidth, height: selectedHeight } : null; + + const connectorBody = { + targetProduct: this.workflowCfg.productName, + ...(this.assetId && { assetId: this.assetId }), + ...(query && { query }), + payload: { + workflow: this.workflowCfg.supportedFeatures.values().next().value, + verb: this.verb, + action: 'asset-upload', + locale: getLocale(), + additionalQueryParams: queryParams, + size, + ...(modelId && { modelId }), + ...(modelVersion && { modelVersion }), + ...(aspectRatio && { aspectRatio }), + generate: connectorGenerate, + }, + }; + try { + const headerExtras = this.getAdditionalHeaders(); + const postOpts = await getApiCallOptions( + 'POST', + unityConfig.apiKey, + headerExtras, + { body: JSON.stringify(connectorBody) }, + ); + const { default: NetworkUtils } = await import(`${getUnityLibs()}/utils/NetworkUtils.js`); + const networkUtils = new NetworkUtils(); + const { url } = await networkUtils.fetchFromService( + this.apiConfig.connectorApiEndPoint, + postOpts, + async (response) => { + if (response.status !== 200) { + const error = new Error('Connector call failed'); + error.status = response.status; + throw error; + } + return response.json(); + }, + ); + this.logAnalytics('Generate Complete|UnityWidget', { assetId: this.assetId }); + this.LOADER_LIMIT = 100; + if (this.transitionScreen?.splashScreenEl) { + this.transitionScreen.LOADER_LIMIT = 100; + this.transitionScreen.updateProgressBar(this.transitionScreen.splashScreenEl, 100); + } + if (url) window.location.href = url; + } catch (err) { + if (err.message === 'Operation termination requested.') return; + await this.transitionScreen?.showSplashScreen(); + this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, err, this.lanaOptions); + this.logAnalytics('Generate Error|UnityWidget', { + errorData: { code: 'request-failed', subCode: err.status, desc: err.message }, + assetId: this.assetId, + }); + window.lana?.log(`Message: Connector call failed, Error: ${err}`, this.lanaOptions); + } + } + + async handlePreloads() { + const parr = []; + if (this.workflowCfg.targetCfg?.showSplashScreen) { + parr.push(`${getUnityLibs()}/core/styles/splash-screen.css`); + } + if (parr.length) await priorityLoad(parr); + } + + isStringActionMap(actMap) { + return actMap && typeof actMap === 'object' && Object.keys(actMap).length > 0 + && Object.values(actMap).every((v) => typeof v === 'string'); + } + + async cancelUploadOperation() { + try { + this.uploadAbortController?.abort(); + this.uploadAbortController = null; + sendAnalyticsEvent(new CustomEvent('Cancel|UnityWidget')); + this.logAnalytics('Cancel|UnityWidget', { assetId: this.assetId }); + await this.ensureTransitionScreen(); + await this.transitionScreen.showSplashScreen(); + const e = new Error('Operation termination requested.'); + const cancelPromise = Promise.reject(e); + cancelPromise.catch(() => {}); + this.promiseStack.unshift(cancelPromise); + } catch (error) { + await this.transitionScreen?.showSplashScreen(); + window.lana?.log(`Message: Error cancelling upload operation, Error: ${error}`, this.lanaOptions); + throw error; + } + } + + async executeActionMaps(value) { + await this.handlePreloads(); + if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast(); + if (value === 'interrupt') await this.cancelUploadOperation(); + } + + async bindStringActionMap(b, actMap) { + const actions = { + A: (el, key) => { + el.addEventListener('click', async (e) => { + const action = actMap[key]; + if (action !== 'redirect') e.preventDefault(); + await this.executeActionMaps(action); + }); + }, + DIV: (el, key) => { + el.addEventListener('drop', async (e) => { + sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget')); + this.preventDefault(e); + const extracted = this.extractFiles(e); + this.filesData = { count: extracted.length, size: extracted[0]?.size, type: extracted[0]?.type }; + this.logAnalytics('Drag and drop|UnityWidget', { assetId: this.assetId, fileMetaData: this.filesData }); + await this.executeActionMaps(actMap[key], extracted); + }); + el.addEventListener('click', () => { + sendAnalyticsEvent(new CustomEvent('Click Drag and drop|UnityWidget')); + }); + }, + INPUT: (el, key) => { + el.addEventListener('click', () => { + this.toastCanvasAreas.forEach((element) => { + const errHolder = element.querySelector('.alert-holder'); + if (errHolder?.classList.contains('show')) { + element.style.pointerEvents = 'auto'; + errHolder.classList.remove('show'); + } + }); + }); + el.addEventListener('change', async (e) => { + const extracted = this.extractFiles(e); + this.filesData = { count: extracted.length, size: extracted[0]?.size, type: extracted[0]?.type }; + this.logAnalytics('Click Drag and drop|UnityWidget', { assetId: this.assetId, fileMetaData: this.filesData }); + await this.executeActionMaps(actMap[key], extracted); + e.target.value = ''; + }); + }, + }; + for (const [key] of Object.entries(actMap)) { + const elements = b.querySelectorAll(key); + if (elements?.length) { + elements.forEach((el) => { + const actionType = el.nodeName; + if (actions[actionType]) actions[actionType](el, key); + }); + } + } + } + + setupServiceHandler() { + this.serviceHandler = new ServiceHandler( + this.workflowCfg.targetCfg?.renderWidget, + this.toastCanvasAreas, + this.unityEl, + this.workflowCfg, + this.getAdditionalHeaders.bind(this), + ); + } + + async initActionListeners(b = this.block, actMap = this.actionMap) { + const searchRoot = this.canvasArea || this.block; + this.widgetWrap = searchRoot?.querySelector?.('.ex-unity-wrap') || this.widgetWrap; + + this.setupServiceHandler(); + await this.initAnalytics(); + + if (this.isStringActionMap(actMap)) { + await this.bindStringActionMap(b, actMap); + return; + } + if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast(); + await this.handlePreloads(); + this.inputField = this.widgetWrap?.querySelector('#pbuPromptInput') + || this.widgetWrap?.querySelector('.inp-field') + || this.inputField; + for (const [selector, actionsList] of Object.entries(actMap)) { + const elements = (this.widgetWrap || searchRoot)?.querySelectorAll(selector); + if (!elements?.length) continue; + elements.forEach((el) => { + if (el.dataset.pbuBound) return; + el.dataset.pbuBound = 'true'; + this.bindElement(el, actionsList); + }); + } + this.inputField?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + (this.widgetWrap || searchRoot)?.querySelector('.gen-btn')?.click(); + } + }); + + const pbuEvents = this.analyticsModule.PROMPT_BAR_EVENTS; + this.bindWidgetInteractionEvent('pbu-enter-prompt', pbuEvents.ENTER_PROMPT, 'enter-prompt'); + this.bindWidgetInteractionEvent('pbu-model-dropdown-open', pbuEvents.MODEL_SELECT_DROPDOWN, 'open'); + this.bindWidgetInteractionEvent('pbu-ratio-dropdown-open', pbuEvents.RATIO_DROPDOWN, 'open'); + this.widgetWrap?.addEventListener('pbu-delete-image', () => this.resetUploadedAssetState({ dropPendingImage: true })); + this.bindOuterMarqueeDropTarget(); + } + + bindOuterMarqueeDropTarget() { + const outerMarquee = this.block?.querySelector('.upload-marquee-layout') || this.block; + const dropZone = this.widgetWrap?.querySelector('.drop-zone'); + if (!outerMarquee || outerMarquee.dataset.pbuOuterDropBound === 'true') return; + outerMarquee.dataset.pbuOuterDropBound = 'true'; + + let dragDepth = 0; + const hasFilePayload = (e) => !!e?.dataTransfer?.types + && Array.from(e.dataTransfer.types).includes('Files'); + const setDropzoneHighlight = (isOn) => dropZone?.classList.toggle('drag-over', !!isOn); + + outerMarquee.addEventListener('dragenter', (e) => { + if (!hasFilePayload(e)) return; + e.preventDefault(); + dragDepth += 1; + setDropzoneHighlight(true); + }); + + outerMarquee.addEventListener('dragover', (e) => { + if (!hasFilePayload(e)) return; + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + setDropzoneHighlight(true); + }); + + outerMarquee.addEventListener('dragleave', (e) => { + if (!hasFilePayload(e)) return; + e.preventDefault(); + dragDepth = Math.max(0, dragDepth - 1); + if (dragDepth === 0) setDropzoneHighlight(false); + }); + + outerMarquee.addEventListener('drop', async (e) => { + if (!hasFilePayload(e)) return; + e.preventDefault(); + dragDepth = 0; + setDropzoneHighlight(false); + sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget')); + const files = this.extractFiles(e); + await this.executeAction('file-selected', outerMarquee, files); + }); + } + + bindElement(el, actionsList) { + const actions = Array.isArray(actionsList) ? actionsList : [actionsList]; + const primaryAction = actions[0]?.actionType; + + switch (el.nodeName) { + case 'A': + case 'BUTTON': + el.addEventListener('click', async (e) => { + e.preventDefault(); + await this.executeAction(primaryAction, el); + }); + break; + case 'DIV': + el.addEventListener('dragover', (e) => { e.preventDefault(); el.classList.add('drag-over'); }); + el.addEventListener('dragleave', () => el.classList.remove('drag-over')); + el.addEventListener('drop', async (e) => { + e.preventDefault(); + el.classList.remove('drag-over'); + sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget')); + const files = this.extractFiles(e); + await this.executeAction(primaryAction, el, files); + }); + el.addEventListener('click', () => { this.block?.querySelector('#file-upload')?.click(); }); + break; + case 'INPUT': + el.addEventListener('change', async (e) => { + const files = this.extractFiles(e); + await this.executeAction(primaryAction, el, files); + e.target.value = ''; + }); + break; + default: + break; + } + } + + async executeAction(actionType, el, files) { + try { + switch (actionType) { + case 'generate': + await this.handleGenerate(true); + break; + case 'more': + await this.handleGenerate(false); + break; + case 'file-selected': + await this.validateAndStoreFile(files); + break; + default: + break; + } + } catch (err) { + window.lana?.log(`Message: Action "${actionType}" failed, Error: ${err}`, this.lanaOptions); + } + } + + bindWidgetInteractionEvent(domEventName, analyticsEventName, action) { + this.widgetWrap?.addEventListener(domEventName, () => { + sendAnalyticsEvent(new CustomEvent(analyticsEventName)); + this.logAnalytics(analyticsEventName, { action }); + }); + } + + preventDefault(e) { + e.preventDefault(); + e.stopPropagation(); + } +} diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg new file mode 100644 index 00000000..b196e215 --- /dev/null +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg @@ -0,0 +1,46 @@ + diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/target-config.json b/unitylibs/core/workflow/workflow-prompt-bar-upload/target-config.json new file mode 100644 index 00000000..cc519002 --- /dev/null +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/target-config.json @@ -0,0 +1,31 @@ +{ + "_defaults": { + "renderWidget": true, + "showSplashScreen": true, + "splashScreenConfig": { + "fragmentLink-firefly": "/cc-shared/fragments/products/firefly/unity/splash-page", + "fragmentLink-firefly-dark": "/cc-shared/fragments/products/firefly/unity/splash-page-dark", + "splashScreenParent": "body" + }, + "limits": { + "maxNumFiles": 1, + "allowedFileTypes": ["image/jpeg", "image/png", "image/jpg"], + "maxFileSize": 50000000, + "max-char-limit": 1024 + }, + "productTag-firefly": "FF", + "sendSplunkAnalytics": true, + "actionMap": { + ".gen-btn": [{ "actionType": "generate" }], + ".more-btn": [{ "actionType": "more" }], + ".drop-zone": [{ "actionType": "file-selected" }], + "#file-upload": [{ "actionType": "file-selected" }] + } + }, + "upload-marquee": { + "selector": ".copy", + "source": ".copy", + "target": ".upload-marquee-prompt-container", + "insert": "after" + } +} diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index d74726f9..ae9c1620 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -84,9 +84,7 @@ export default class ActionBinder { this.splashScreenEl = null; this.transitionScreen = null; this.LOADER_LIMIT = 95; - const commonLimits = workflowCfg.targetCfg.limits || {}; - const productLimits = workflowCfg.targetCfg[`limits-${workflowCfg.productName.toLowerCase()}`] || {}; - this.limits = { ...commonLimits, ...productLimits }; + this.limits = ActionBinder.resolveLimits(workflowCfg); this.promiseStack = []; this.initActionListeners = this.initActionListeners.bind(this); const productTag = workflowCfg.targetCfg[`productTag-${workflowCfg.productName.toLowerCase()}`] || 'UNKNOWN'; @@ -99,6 +97,18 @@ export default class ActionBinder { this.uploadAbortController = null; } + static resolveLimits(workflowCfg) { + const targetCfg = workflowCfg.targetCfg || {}; + const commonLimits = targetCfg.limits || {}; + const productLimits = targetCfg[`limits-${workflowCfg.productName?.toLowerCase()}`] || {}; + const featureLimits = Array.from(workflowCfg.supportedFeatures || []).reduce((acc, feature) => ({ + ...acc, + ...(targetCfg[`limits-${feature}`] || {}), + }), {}); + const hasFeatureLimits = Object.keys(featureLimits).length > 0; + return { ...commonLimits, ...(hasFeatureLimits ? featureLimits : productLimits) }; + } + getApiConfig() { unityConfig.endPoint = { assetUpload: `${unityConfig.apiEndPoint}/asset`, @@ -403,6 +413,21 @@ export default class ActionBinder { return { width, height }; } + async checkVideoDuration(file) { + const { getVideoDuration } = await import(`${getUnityLibs()}/utils/FileUtils.js`); + const duration = await getVideoDuration(file); + this.filesData = { ...this.filesData, duration }; + if (this.limits.minDuration && duration < this.limits.minDuration) { + this.handleClientUploadError('.icon-error-videominduration', 'error-minVideoduration', 'Video is too short'); + throw new Error('Video is too short'); + } + if (this.limits.maxDuration && duration > this.limits.maxDuration) { + this.handleClientUploadError('.icon-error-videomaxduration', 'error-maxVideoduration', 'Video is too long'); + throw new Error('Video is too long'); + } + return duration; + } + async initAnalytics() { if (!this.sendAnalyticsToSplunk && this.workflowCfg.targetCfg.sendSplunkAnalytics) { this.sendAnalyticsToSplunk = (await import(`${getUnityLibs()}/scripts/analytics.js`)).default; @@ -420,14 +445,14 @@ export default class ActionBinder { this.logAnalyticsinSplunk('Upload client error|UnityWidget', { errorData: { code: errorCode }, fileMetaData: this.filesData, action: 'upload' }); } - async uploadImage(files) { + async uploadFile(files) { if (!files) return; const file = files[0]; if (this.limits.maxNumFiles !== files.length) { this.handleClientUploadError('.icon-error-filecount', 'error-filecount'); return; } - if (!this.limits.allowedFileTypes.includes(file.type)) { + if (!this.limits.allowedFileTypes?.includes(file.type)) { this.handleClientUploadError('.icon-error-filetype', 'error-filetype'); return; } @@ -435,8 +460,12 @@ export default class ActionBinder { this.handleClientUploadError('.icon-error-filesize', 'error-filesize'); return; } - try { await this.checkImageDimensions(file); } catch (error) { - window.lana?.log(`Message: Error checking image dimensions, Error: ${error}`, this.lanaOptions); + const isVideo = file.type.startsWith('video/'); + try { + if (isVideo) { await this.checkVideoDuration(file);} + else { await this.checkImageDimensions(file);} + } catch (error) { + window.lana?.log(`Message: Error checking file constraints, Error: ${error}`, this.lanaOptions); return; } sendAnalyticsEvent(new CustomEvent('Uploading Started|UnityWidget')); @@ -475,7 +504,7 @@ export default class ActionBinder { switch (value) { case 'upload': this.promiseStack = []; - await this.uploadImage(files); + await this.uploadFile(files); break; case 'interrupt': await this.cancelUploadOperation(); @@ -519,6 +548,9 @@ export default class ActionBinder { }); }, INPUT: (el, key) => { + if (this.limits.allowedFileTypes?.length) { + el.setAttribute('accept', this.limits.allowedFileTypes.join(',')); + } el.addEventListener('click', () => { this.canvasArea.forEach((element) => { const errHolder = element.querySelector('.alert-holder'); diff --git a/unitylibs/core/workflow/workflow-upload/target-config.json b/unitylibs/core/workflow/workflow-upload/target-config.json index 0648b0df..9f6326f8 100644 --- a/unitylibs/core/workflow/workflow-upload/target-config.json +++ b/unitylibs/core/workflow/workflow-upload/target-config.json @@ -28,6 +28,12 @@ "minHeight": 512, "minWidth": 512 }, + "limits-upload-video": { + "allowedFileTypes": ["video/mp4", "video/quicktime"], + "maxFileSize": 200000000, + "minDuration": 5, + "maxDuration": 20 + }, "showSplashScreen": true, "splashScreenConfig": { "fragmentLink-photoshop": "/cc-shared/fragments/products/photoshop/unity/splash-page/splashscreen", diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js index 1a726663..8c6b9a24 100644 --- a/unitylibs/core/workflow/workflow.js +++ b/unitylibs/core/workflow/workflow.js @@ -26,29 +26,20 @@ class WfInitiator { return cls ? cls.replace(/^widget-/, '') : 'prompt-bar'; } - static getWidgetRegistry() { - const widgetBase = `${getUnityLibs()}/core/widgets`; - return { - 'prompt-bar': [`${widgetBase}/prompt-bar/prompt-bar.js`, `${widgetBase}/prompt-bar/prompt-bar.css`], - 'prompt-bar-style': [ - `${widgetBase}/prompt-bar-style/prompt-bar-style.js`, - `${widgetBase}/prompt-bar-style/prompt-bar-style.css`, - ], - }; + static widgetPathsForName(name) { + const widgetBase = `${getUnityLibs()}/core/widgets/${name}`; + return [`${widgetBase}/${name}.js`, `${widgetBase}/${name}.css`]; } getWidgetPaths() { this.widgetName = this.getWidgetNameFromClass(); - const registry = WfInitiator.getWidgetRegistry(); - return registry[this.widgetName] || registry['prompt-bar']; + return WfInitiator.widgetPathsForName(this.widgetName); } static getWidgetPathsFromEl(el) { - const registry = WfInitiator.getWidgetRegistry(); - if (!el) return registry['prompt-bar']; - const cls = [...el.classList].find((c) => c.startsWith('widget-')); - const rawName = cls ? cls.replace(/^widget-/, '') : 'prompt-bar'; - return registry[rawName] || registry['prompt-bar']; + const cls = el && [...el.classList].find((c) => c.startsWith('widget-')); + const name = cls ? cls.replace(/^widget-/, '') : 'prompt-bar'; + return WfInitiator.widgetPathsForName(name); } async priorityLibFetch(workflowName) { @@ -67,6 +58,10 @@ class WfInitiator { ], 'workflow-ai': [...bundledWidgetAssets], 'workflow-firefly': fireflyShared, + 'workflow-prompt-bar-upload': [ + `${baseWfPath}/sprite.svg`, + ...this.getWidgetPaths(), + ], }; const commonResources = [ `${baseWfPath}/target-config.json`, @@ -101,7 +96,7 @@ class WfInitiator { if (this.targetConfig.renderWidget) { const widgetPath = (this.workflowCfg.name === 'workflow-photoshop' || this.workflowCfg.name === 'workflow-ai') ? `${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/widget.js` - : WfInitiator.getWidgetRegistry()[this.widgetName][0]; + : WfInitiator.widgetPathsForName(this.widgetName)[0]; const { default: UnityWidget } = await import(widgetPath); const spriteContent = await spriteSvg.text(); unityWidgetObject = new UnityWidget( @@ -267,6 +262,10 @@ class WfInitiator { sfList: new Set([feature]), psw, }, + 'workflow-prompt-bar-upload': { + productName: product || 'Firefly', + sfList: new Set([feature || 'image-to-video']), + }, 'workflow-firefly': { productName: 'Firefly', sfList: new Set(['text-to-mage']), diff --git a/unitylibs/img/icons/upload.svg b/unitylibs/img/icons/upload.svg new file mode 100755 index 00000000..bdec86ab --- /dev/null +++ b/unitylibs/img/icons/upload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/unitylibs/scripts/analytics.js b/unitylibs/scripts/analytics.js index 628fd0d2..f982ddf3 100644 --- a/unitylibs/scripts/analytics.js +++ b/unitylibs/scripts/analytics.js @@ -1,10 +1,18 @@ -export const PROMPT_WITH_STYLE_EVENTS = { +export const PROMPT_BAR_EVENTS = { ENTER_PROMPT: 'Enter Prompt|UnityWidget', MODEL_SELECT_DROPDOWN: 'Model Select Dropdown|UnityWidget', GENERATE_CTA: 'Click on Generate CTA|UnityWidget', MODULE_PICKER: 'Module Picker Select Dropdown|UnityWidget', + RATIO_DROPDOWN: 'Ratio Dropdown Select|UnityWidget', + MORE: 'More|UnityWidget', + UPLOAD_STARTED: 'Uploading started|UnityWidget', + UPLOAD_ERROR: 'Upload error|UnityWidget', + generateModel: (modelName) => `Generate ${modelName}|UnityWidget`, + ratioSelect: (ratio) => `${ratio}|UnityWidget`, }; +export const PROMPT_WITH_STYLE_EVENTS = PROMPT_BAR_EVENTS; + export function styleSelectionGenerateEventName(styleIndexOneBased) { return `Style ${styleIndexOneBased}|UnityWidget`; } @@ -28,7 +36,7 @@ function getSessionID() { function createPayloadForSplunk(metaData) { const { eventName, product, errorData, redirectUrl, assetId, statusCode, verb, action, workflowStep, fileMetaData, operation, - styleEventName, modelGenEventName, + styleEventName, modelGenEventName, aspectRatio, hasImage, } = metaData; return { event: { @@ -45,6 +53,8 @@ function createPayloadForSplunk(metaData) { ...(fileMetaData && { fileMetaData }), ...(styleEventName && { style: styleEventName }), ...(modelGenEventName && { model: modelGenEventName }), + ...(aspectRatio && { aspectRatio }), + ...(hasImage !== undefined && hasImage !== null && { hasImage }), }, source: { user_agent: navigator.userAgent, diff --git a/unitylibs/scripts/transition-screen.js b/unitylibs/scripts/transition-screen.js index 800b1642..2f9c83d6 100644 --- a/unitylibs/scripts/transition-screen.js +++ b/unitylibs/scripts/transition-screen.js @@ -83,7 +83,7 @@ export default class TransitionScreen { return splashScreenConfig[`fragmentLink-${matchedDomain}`]; } const productName = this.workflowCfg.productName.toLowerCase(); - if (this.workflowCfg.name === 'workflow-upload') { + if (this.workflowCfg.name === 'workflow-upload' || this.workflowCfg.name === 'workflow-prompt-bar-upload') { const { theme } = this.workflowCfg; const themedKey = theme ? `fragmentLink-${productName}-${theme}` : null; if (themedKey && splashScreenConfig[themedKey]) return splashScreenConfig[themedKey]; @@ -113,7 +113,9 @@ export default class TransitionScreen { async loadSplashFragment() { if (!this.workflowCfg.targetCfg.showSplashScreen) return; + if (this.splashScreenEl) return; const fragmentLink = this.getFragmentLink(); + if (!fragmentLink) return; this.splashFragmentLink = localizeLink(`${window.location.origin}${fragmentLink}`); const resp = await fetch(`${this.splashFragmentLink}.plain.html`); const html = await resp.text(); diff --git a/unitylibs/utils/FileUtils.js b/unitylibs/utils/FileUtils.js index f8976215..8917cd8c 100644 --- a/unitylibs/utils/FileUtils.js +++ b/unitylibs/utils/FileUtils.js @@ -24,6 +24,23 @@ export function getMimeType(fileName) { return extToTypeMap[getExtension(fileName)]; } +export function getVideoDuration(file) { + return new Promise((resolve, reject) => { + const url = URL.createObjectURL(file); + const video = document.createElement('video'); + video.preload = 'metadata'; + video.onloadedmetadata = () => { + URL.revokeObjectURL(url); + resolve(video.duration); + }; + video.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error(`Unable to read video metadata for: ${file.name}`)); + }; + video.src = url; + }); +} + export async function getImageDimensions(file) { const buffer = await file.slice(0, 256 * 1024).arrayBuffer(); const view = new DataView(buffer); From 9d0bb317cd64ac417a4610c9eec270f55b11824e Mon Sep 17 00:00:00 2001 From: Nishant Thakur Date: Mon, 4 May 2026 12:45:47 +0530 Subject: [PATCH 09/18] MWPW-191633: Enable milolibs/unitylibs query params on CC stage URLs (#754) * Enable milolibs/unitylibs query params on stage URLs Resolves: [MWPW-191633](https://jira.corp.adobe.com/browse/MWPW-191633) **Test URLs:** - Before: https://main--cc--adobecom.aem.live/?martech=off - After: https://MWPW-191633-support-milolibs-stage--cc--adobecom.aem.live/?martech=off > Note that this has a supporting PR in `adobecom/da-cc` as well at https://github.com/adobecom/da-cc/pull/45 **Dev validation:** (using browser overrides) URL 1: https://www.stage.adobe.com/products/firefly/features/remove-person-from-photo.html?milolibs=ratko-test&unitylibs=doodlebug-v152-2 URLs by domain: [ff-doodlebug_milolibs_unitylibs_js_urls.md](https://github.com/user-attachments/files/27097349/ff-doodlebug_milolibs_unitylibs_js_urls.md) URL 2: https://www.stage.adobe.com/creativecloud/plans.html?milolibs=ratko-test&unitylibs=doodlebug-v152-2 URLs by domain: [plans_milolibs_js_urls.md](https://github.com/user-attachments/files/27097352/plans_milolibs_js_urls.md) URL 3: https://www.stage.adobe.com/products/firefly/features/remove-person-from-photo.html?milolibs=ratko-test URLs by domain: [doodlebug_milolibs_js_urls.md](https://github.com/user-attachments/files/27097356/doodlebug_milolibs_js_urls.md) URL 4: https://www.stage.adobe.com/products/firefly/features/remove-person-from-photo.html?unitylibs=doodlebug-v152-2 URLs by domain: [doodlebug_unitylibs_js_urls.md](https://github.com/user-attachments/files/27097358/doodlebug_unitylibs_js_urls.md) --- unitylibs/scripts/utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index ce6cbc44..c18fbe6b 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -5,9 +5,10 @@ export const [setLibs, getLibs] = (() => { libs = (() => { const { hostname, origin, search } = location || window.location; if (hostname.endsWith('acrobat.adobe.com')) return `${origin}/dc-shared/libs`; - if (!(hostname.includes('.hlx.') || hostname.includes('.aem.') || hostname.includes('local'))) return prodLibs; + if (!['.aem.', '.hlx.', '.stage.', 'localhost', '.da.'].some((i) => hostname.includes(i))) return prodLibs; const branch = new URLSearchParams(search).get('milolibs') || 'main'; if (branch === 'local') return 'http://localhost:6456/libs'; + if (branch === 'main' && hostname.includes('.stage.')) return prodLibs; const env = hostname.includes('.hlx.') ? 'hlx' : 'aem'; return branch.includes('--') ? `https://${branch}.${env}.live/libs` : `https://${branch}--milo--adobecom.${env}.live/libs`; })(); @@ -29,7 +30,7 @@ export const [setUnityLibs, getUnityLibs] = (() => { export function decorateArea() {} -const miloLibs = setLibs('/libs'); +const miloLibs = setLibs(`${window.location.origin}/libs`); const { createTag, getConfig, loadStyle, loadLink, loadScript, localizeLink, loadArea, From 8910ad62b731f31812ae93b2569cdf80d9506451 Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Tue, 5 May 2026 11:42:10 +0530 Subject: [PATCH 10/18] Bug fixes + Added aspect ratio icons (#762) Resolves: [MWPW-194052](https://jira.corp.adobe.com/browse/MWPW-194052) [MWPW-193773](https://jira.corp.adobe.com/browse/MWPW-193773) [MWPW-193716](https://jira.corp.adobe.com/browse/MWPW-193716) [MWPW-194279](https://jira.corp.adobe.com/browse/MWPW-194279) [MWPW-194149](https://jira.corp.adobe.com/browse/MWPW-194149) **Test URLs:** - Before: https://main--da-cc--adobecom.aem.page/drafts/arugupta/image-to-video/doodlebug/image-to-video?unitylibs=stage - After: https://main--da-cc--adobecom.aem.page/drafts/arugupta/image-to-video/doodlebug/image-to-video?unitylibs=wave2vid-followup --------- Co-authored-by: Arushi Gupta --- .../prompt-bar-upload/prompt-bar-upload.css | 156 ++++++++++++++---- .../prompt-bar-upload/prompt-bar-upload.js | 22 ++- .../action-binder.js | 12 +- .../workflow-prompt-bar-upload/sprite.svg | 6 + 4 files changed, 153 insertions(+), 43 deletions(-) diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css index b29feb7c..9d422f9b 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css @@ -5,23 +5,30 @@ .unity-prompt-bar-upload.unity-enabled { width: 100%; -max-width: 1000px; +max-width: 100%; margin-top: 24px; +box-sizing: border-box; } .unity-prompt-bar-upload.unity-enabled .interactive-area { -width: 727px; +width: 100%; +max-width: 100%; background: #222 !important; border: none !important; box-shadow: none !important; border-radius: 20px !important; padding: 8px !important; box-sizing: border-box !important; -max-width: 727px; } @media screen and (min-width: 1200px) { +.unity-prompt-bar-upload.unity-enabled { + max-width: 1000px; +} + .unity-prompt-bar-upload.unity-enabled .interactive-area { + width: 727px; + max-width: 727px; padding: 14px !important; } } @@ -29,7 +36,7 @@ max-width: 727px; .ex-unity-wrap.pbu-widget .pbu-legal-foot { font-size: 14px; width: fit-content; -max-width: 727px; +max-width: 100%; box-sizing: border-box; font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; font-size: 13px; @@ -85,6 +92,8 @@ min-width: 0; .unity-prompt-bar-upload.unity-enabled .pbu-right-section .pbu-prompt-bar-container { display: flex; flex-direction: column; +align-items: stretch; +align-content: stretch; gap: 0; width: 100%; min-width: 0; @@ -124,9 +133,18 @@ max-width: 100%; .unity-prompt-bar-upload.unity-enabled .pbu-right-section .unity-slf-prompt-label { margin-bottom: 0px; +margin-inline: 0; padding: 0; +padding-inline: 0; +-webkit-padding-start: 0; +-webkit-padding-end: 0; display: block; flex-shrink: 0; +align-self: stretch; +width: 100%; +max-width: 100%; +box-sizing: border-box; +text-align: start; } .unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field { @@ -134,14 +152,56 @@ flex: 1; min-height: 80px; resize: none; width: 100%; +max-width: 100%; +align-self: stretch; box-sizing: border-box; -padding-bottom: 10px; +padding: 0; +padding-inline: 0; +padding-block: 0 10px; +-webkit-padding-start: 0; +-webkit-padding-end: 0; margin: 0 0 12px 0; +margin-inline: 0; border: none; background: transparent; outline: none; font-size: var(--type-body-s-size, 15px); line-height: 1.45; +text-align: start; +-webkit-appearance: none; +appearance: none; +scrollbar-width: thin; +scrollbar-color: #888 transparent; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field::-webkit-scrollbar { +width: 6px; +height: 6px; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field::-webkit-scrollbar-track { +background: transparent; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field::-webkit-scrollbar-thumb { +background-color: #888; +border-radius: 6px; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field::-webkit-scrollbar-thumb:hover { +background-color: #a0a0a0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-right-section .inp-field { +scrollbar-color: #6a6a6a rgb(0 0 0 / 12%); +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-right-section .inp-field::-webkit-scrollbar-thumb { +background-color: #6a6a6a; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-right-section .inp-field::-webkit-scrollbar-thumb:hover { +background-color: #555; } .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer { @@ -189,6 +249,12 @@ line-height: 18px; .unity-prompt-bar-upload.unity-enabled .unity-slf-prompt-label { display: block; margin-bottom: 8px; +margin-inline: 0; +padding-inline: 0; +text-align: start; +width: 100%; +max-width: 100%; +box-sizing: border-box; } .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .inp-field, @@ -239,6 +305,42 @@ width: 22px; height: 22px; } +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .pbu-aspect-ratio-icon, +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .pbu-aspect-ratio-icon { +display: flex; +align-items: center; +justify-content: center; +flex-shrink: 0; +font-size: 0; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .pbu-aspect-ratio-icon svg, +.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .pbu-aspect-ratio-icon svg { +width: 20px; +height: 20px; +display: block; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model .pbu-aspect-ratio-icon { +color: #fff; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model .pbu-aspect-ratio-icon { +color: #292929; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model .pbu-aspect-ratio-icon svg { +opacity: 0.95; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .pbu-aspect-ratio-icon { +color: #f8f8f8; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area.light .verb-list .verb-link .pbu-aspect-ratio-icon { +color: #292929; +} + .unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon, .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon { font-size: 0; @@ -341,17 +443,6 @@ flex-shrink: 0; object-fit: cover; } -.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-aspect-models .selected-model:not(:has(img))::before { -content: ''; -display: block; -width: 22px; -height: 12px; -box-sizing: border-box; -border: 1.5px solid rgb(255 255 255 / 92%); -border-radius: 2px; -flex-shrink: 0; -} - .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model { display: inline-flex; align-items: center; @@ -380,17 +471,6 @@ filter: brightness(0); opacity: 0.85; } -.unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-aspect-models .selected-model:not(:has(img))::before { -content: ''; -display: block; -width: 22px; -height: 12px; -box-sizing: border-box; -border: 1.5px solid rgb(0 0 0 / 55%); -border-radius: 2px; -flex-shrink: 0; -} - :root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model, :root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model { border-radius: 12px; @@ -569,12 +649,19 @@ font-weight: 400; .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list, .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container .verb-list { -top: auto; -bottom: 100%; +top: 100%; +bottom: auto; +left: 0; transform: none; animation: none; -margin-bottom: 4px; -z-index: 10; +margin-top: 4px; +margin-bottom: 0; +} + +[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list, +[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container .verb-list { +left: auto; +right: 0; } .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container, @@ -583,9 +670,9 @@ position: relative; z-index: 1; } -.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container.show-menu, -.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container.show-menu { -z-index: 300; +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container.show-menu .verb-list, +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container.show-menu .verb-list { +animation: none; } .pbu-drop-zone-wrap { @@ -896,6 +983,7 @@ line-height: normal; } .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap .gen-btn { + max-width: 130px; flex: 1; justify-content: center; } diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js index 4389b5f9..f9a28fbd 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -31,6 +31,21 @@ function svgIcon(href) { return ``; } +function getAspectRatioIconHref(ratio) { + const parts = String(ratio).split(':').map((s) => parseFloat(String(s).trim(), 10)); + if (parts.length !== 2 || Number.isNaN(parts[0]) || Number.isNaN(parts[1]) || parts[1] === 0) { + return '#unity-aspect-ratio-horizontal-icon'; + } + const [w, h] = parts; + if (w > h) return '#unity-aspect-ratio-horizontal-icon'; + if (h > w) return '#unity-aspect-ratio-vertical-icon'; + return '#unity-aspect-ratio-horizontal-icon'; +} + +function createAspectRatioIconSpan(ratio) { + return createTag('span', { class: 'pbu-aspect-ratio-icon', 'aria-hidden': 'true' }, svgIcon(getAspectRatioIconHref(ratio))); +} + function syncDropdownSelection(list, activeLink) { list.querySelectorAll('li').forEach((li) => { const a = li.querySelector('a'); @@ -353,10 +368,12 @@ export default class PromptBarUploadWidget { if (!ratios.length) return null; this.setSelectedAspectRatio(modelId, ratios[0]); + const triggerAspectIcon = createAspectRatioIconSpan(ratios[0]); const { container, triggerBtn, nameContainer, list } = buildDropdownShell({ label: 'Aspect ratio', menuId: 'pbu-aspect-menu', extraClass: 'pbu-aspect-models', + imgEl: triggerAspectIcon, }); nameContainer.textContent = ratios[0]; @@ -369,7 +386,7 @@ export default class PromptBarUploadWidget { 'aria-selected': idx === 0 ? 'true' : 'false', role: 'option', }); - link.append(selectedIcon, createTag('span', { class: 'model-name' }, ratio)); + link.append(selectedIcon, createAspectRatioIconSpan(ratio), createTag('span', { class: 'model-name' }, ratio)); const li = createTag('li', { class: `verb-item${idx === 0 ? ' selected' : ''}`, role: 'presentation' }); li.append(link); list.append(li); @@ -382,6 +399,7 @@ export default class PromptBarUploadWidget { e.stopPropagation(); const ratio = link.getAttribute('data-ratio') || ''; nameContainer.textContent = ratio; + triggerAspectIcon.innerHTML = svgIcon(getAspectRatioIconHref(ratio)); this.setSelectedAspectRatio(modelId, ratio); syncDropdownSelection(list, link); closeDropdown(container, triggerBtn, list); @@ -492,12 +510,10 @@ export default class PromptBarUploadWidget { buildPromptTextarea() { const defaultPrompt = placeholderText(this.el, 'icon-default-prompt') || ''; - const maxCharLimit = this.workflowCfg?.targetCfg?.limits?.['max-char-limit'] ?? 750; const textarea = createTag('textarea', { id: 'pbuPromptInput', class: 'inp-field', rows: '1', - maxlength: String(maxCharLimit), 'aria-label': defaultPrompt, 'aria-autocomplete': 'list', }); diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js index 3b43cbce..1caeb049 100644 --- a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js @@ -353,7 +353,7 @@ export default class ActionBinder { } validateInput(query) { - const maxCharLimit = this.limits?.['max-char-limit'] ?? 750; + const maxCharLimit = this.limits?.['max-char-limit'] ?? 1024; if (query.length > maxCharLimit) { this.handleClientError('.icon-error-max-length', 'max-prompt-characters-exceeded', 'Prompt too long'); this.logAnalytics('generate', { errorData: { code: 'max-prompt-characters-exceeded' } }); @@ -372,7 +372,7 @@ export default class ActionBinder { } } - async handleGenerate(connectorGenerate = true) { + async handleGenerate(isGenerateCta = true) { this.promiseStack = []; if (!this.analyticsModule) await this.initAnalytics(); const pbuEvents = this.analyticsModule.PROMPT_BAR_EVENTS; @@ -382,7 +382,7 @@ export default class ActionBinder { const selectedModelId = this.widgetWrap?.getAttribute('data-selected-model-id') || ''; const selectedAspectRatio = this.widgetWrap?.getAttribute('data-selected-aspect-ratio') || ''; const selectedModelName = this.widgetWrap?.getAttribute('data-selected-model-name') || selectedModelId; - const ctaEventName = connectorGenerate ? pbuEvents.GENERATE_CTA : pbuEvents.MORE; + const ctaEventName = isGenerateCta ? pbuEvents.GENERATE_CTA : pbuEvents.MORE; sendAnalyticsEvent(new CustomEvent(pbuEvents.UPLOAD_STARTED)); sendAnalyticsEvent(new CustomEvent(ctaEventName)); if (selectedModelName) sendAnalyticsEvent(new CustomEvent(pbuEvents.generateModel(selectedModelName))); @@ -410,11 +410,11 @@ export default class ActionBinder { return; } } - await this.continueInApp(query, selectedModelId, selectedAspectRatio, connectorGenerate); + await this.continueInApp(query, selectedModelId, selectedAspectRatio); } - async continueInApp(query, modelId, aspectRatio, connectorGenerate = true) { + async continueInApp(query, modelId, aspectRatio) { const { getCgenQueryParams } = await import(`${getUnityLibs()}/utils/cgen-utils.js`); const queryParams = getCgenQueryParams(this.unityEl); const modelVersion = this.widgetWrap?.getAttribute('data-selected-model-version') || ''; @@ -436,7 +436,7 @@ export default class ActionBinder { ...(modelId && { modelId }), ...(modelVersion && { modelVersion }), ...(aspectRatio && { aspectRatio }), - generate: connectorGenerate, + generate: false, }, }; try { diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg index b196e215..83060d72 100644 --- a/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg @@ -38,6 +38,12 @@ + + + + + + From 1531b9391d27ff7f54b0de6229038aba8c872350 Mon Sep 17 00:00:00 2001 From: Ratko Zagorac <90400759+zagi25@users.noreply.github.com> Date: Tue, 5 May 2026 16:14:10 +0200 Subject: [PATCH 11/18] MWPW-192736: Add check for milolibs query param (#746) Whitelist branch parameter with `/^[a-zA-Z0-9_-]+$/`; throw on any other characters. ## Ticket https://jira.corp.adobe.com/browse/MWPW-192736 ## Test URLs Before: https://stage--unity--adobecom.aem.page/ After: https://MWPW-192736--unity--zagi25.aem.page/ --- _This PR was generated by Claude (Anthropic's Claude Code CLI)._ Co-authored-by: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com> --- unitylibs/blocks/unity/unity.js | 1 + unitylibs/scripts/utils.js | 1 + 2 files changed, 2 insertions(+) diff --git a/unitylibs/blocks/unity/unity.js b/unitylibs/blocks/unity/unity.js index b52e425f..4c9a896b 100644 --- a/unitylibs/blocks/unity/unity.js +++ b/unitylibs/blocks/unity/unity.js @@ -9,6 +9,7 @@ function getUnityLibs(prodLibs, project = 'unity') { return prodLibs; } const branch = new URLSearchParams(window.location.search).get('unitylibs') || 'main'; + if (!/^[a-zA-Z0-9_-]+$/.test(branch)) throw new Error('Invalid branch name.'); const helixVersion = hostname.includes('.hlx.') ? 'hlx' : 'aem'; return branch.indexOf('--') > -1 ? `https://${branch}.${helixVersion}.live/unitylibs` diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index c18fbe6b..31272fcc 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -7,6 +7,7 @@ export const [setLibs, getLibs] = (() => { if (hostname.endsWith('acrobat.adobe.com')) return `${origin}/dc-shared/libs`; if (!['.aem.', '.hlx.', '.stage.', 'localhost', '.da.'].some((i) => hostname.includes(i))) return prodLibs; const branch = new URLSearchParams(search).get('milolibs') || 'main'; + if (!/^[a-zA-Z0-9_-]+$/.test(branch)) throw new Error('Invalid branch name.'); if (branch === 'local') return 'http://localhost:6456/libs'; if (branch === 'main' && hostname.includes('.stage.')) return prodLibs; const env = hostname.includes('.hlx.') ? 'hlx' : 'aem'; From 68788cdd1b712e6c34fc8047feb3598c6207a2d8 Mon Sep 17 00:00:00 2001 From: Manasvi Agrawal Date: Wed, 6 May 2026 21:42:05 +0530 Subject: [PATCH 12/18] [MWPW-192382] Onboard Mindmap Maker for Study Space (#763) - Onboard new verb: mindmap-maker for study space Resolves: [MWPW-192382](https://jira.corp.adobe.com/browse/MWPW-192382) **Test URLs:** https://stage--da-dc--adobecom.aem.live/drafts/maagrawal/study-space/mindmap-maker-test https://stage--da-dc--adobecom.aem.page/drafts/maagrawal/study-space/mindmap-maker-test?unitylibs=unity-mindmap-maker Error sheet for mindmap maker: https://main--unity--adobecom.aem.page/unity/configs/errors/mindmap-maker.json --------- Co-authored-by: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com> --- .../workflow-acrobat/action-binder.test.js | 115 ++++++++++++++++++ .../workflow-acrobat/action-binder.js | 1 + .../workflow-acrobat/target-config.json | 4 +- unitylibs/core/workflow/workflow.js | 1 + 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/test/core/workflow/workflow-acrobat/action-binder.test.js b/test/core/workflow/workflow-acrobat/action-binder.test.js index 1de88fae..6dc4f152 100644 --- a/test/core/workflow/workflow-acrobat/action-binder.test.js +++ b/test/core/workflow/workflow-acrobat/action-binder.test.js @@ -302,6 +302,14 @@ describe('ActionBinder', () => { it('should return false for flashcard-maker with image/jpeg', () => { expect(actionBinder.isSameFileType('flashcard-maker', 'image/jpeg')).to.be.false; }); + + it('should return false for mindmap-maker with application/pdf', () => { + expect(actionBinder.isSameFileType('mindmap-maker', 'application/pdf')).to.be.false; + }); + + it('should return false for mindmap-maker with image/jpeg', () => { + expect(actionBinder.isSameFileType('mindmap-maker', 'image/jpeg')).to.be.false; + }); }); describe('validateFiles', () => { @@ -1034,6 +1042,21 @@ describe('ActionBinder', () => { expect(result).to.be.true; }); + it('should handle redirect for returning user for mindmap-maker', async () => { + actionBinder.workflowCfg.enabledFeatures = ['mindmap-maker']; + localStorage.setItem('unity.user', 'test-user'); + localStorage.setItem('mindmap-maker_attempts', '2'); + + const cOpts = { payload: {} }; + const filesData = { test: 'data' }; + const result = await actionBinder.handleRedirect(cOpts, filesData); + + expect(cOpts.payload.newUser).to.be.false; + expect(cOpts.payload.attempts).to.equal('2+'); + expect(actionBinder.getRedirectUrl.calledWith(cOpts)).to.be.true; + expect(result).to.be.true; + }); + it('should handle redirect with feedback for multi-file validation failure', async () => { actionBinder.multiFileValidationFailure = true; const cOpts = { payload: {} }; @@ -1303,6 +1326,29 @@ describe('ActionBinder', () => { expect(actionBinder.handleMultiFileUpload.calledWith(validFile)).to.be.true; expect(actionBinder.handleSingleFileUpload.called).to.be.false; }); + + it('should handle verbs that require multi-file upload for mindmap-maker', async () => { + actionBinder.workflowCfg = { + name: 'workflow-acrobat', + enabledFeatures: ['mindmap-maker'], + targetCfg: { verbsWithoutMfuToSfuFallback: ['compress-pdf', 'mindmap-maker'] }, + }; + const files = [ + { name: 'test1.pdf', type: 'application/pdf', size: 1048576 }, + { name: 'test2.pdf', type: 'application/pdf', size: 2097152 }, + ]; + const validFile = [files[0]]; + actionBinder.validateFiles.resolves({ isValid: true, validFiles: validFile }); + + await actionBinder.handleFileUpload(files); + + expect(actionBinder.sanitizeFileName.calledTwice).to.be.true; + expect(actionBinder.filterFilesWithPdflite.called).to.be.true; + expect(actionBinder.validateFiles.called).to.be.true; + expect(actionBinder.initUploadHandler.called).to.be.true; + expect(actionBinder.handleMultiFileUpload.calledWith(validFile)).to.be.true; + expect(actionBinder.handleSingleFileUpload.called).to.be.false; + }); }); describe('continueInApp', () => { @@ -1556,6 +1602,35 @@ describe('ActionBinder', () => { extractSpy.restore(); }); + it('should handle input change event with single file for mindmap-maker', async () => { + const el = document.createElement('input'); + el.type = 'file'; + const addEventListenerSpy = sinon.spy(el, 'addEventListener'); + const block = { querySelector: sinon.stub().returns(el) }; + const actMap = { input: 'upload' }; + const extractSpy = sinon.spy(actionBinder, 'extractFiles'); + const spy = sinon.spy(actionBinder, 'acrobatActionMaps'); + + await actionBinder.initActionListeners(block, actMap); + + const handler = addEventListenerSpy.getCalls().find((call) => call.args[0] === 'change').args[1]; + actionBinder.signedOut = false; + actionBinder.tokenError = null; + actionBinder.workflowCfg.enabledFeatures = ['mindmap-maker']; + + const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); + const event = { target: { files: [file], value: '' } }; + + await handler(event); + + expect(extractSpy.called).to.be.true; + expect(spy.called).to.be.true; + expect(spy.firstCall.args).to.deep.equal(['upload', [file], file.size, 'change']); + + spy.restore(); + extractSpy.restore(); + }); + it('should handle input element not found', async () => { const block = { querySelector: sinon.stub().returns(null) }; const actMap = { 'nonexistent-input': 'upload' }; @@ -1802,6 +1877,26 @@ describe('ActionBinder', () => { { code: 'pre_upload_error_missing_verb_config' }, )).to.be.true; }); + + it('should not dispatch error when enabledFeatures[0] is mindmap-maker', async () => { + actionBinder.dispatchErrorToast.resetHistory(); + actionBinder.processSingleFile = sinon.stub().resolves(); + actionBinder.processHybrid = sinon.stub().resolves(); + actionBinder.workflowCfg.enabledFeatures = ['mindmap-maker']; + const validFiles = [ + { name: 'test.pdf', type: 'application/pdf', size: 1048576 }, + ]; + const totalFileSize = validFiles.reduce((sum, file) => sum + file.size, 0); + await actionBinder.acrobatActionMaps('upload', validFiles, totalFileSize, 'test-event'); + expect(actionBinder.dispatchErrorToast.neverCalledWith( + 'error_generic', + 500, + 'Invalid or missing verb configuration on Unity', + false, + true, + { code: 'pre_upload_error_missing_verb_config' }, + )).to.be.true; + }); }); }); @@ -2006,6 +2101,26 @@ describe('ActionBinder', () => { localStorageStub.restore(); actionBinder.getRedirectUrl.restore(); }); + + it('should handle localStorage access error for mindmap-maker', async () => { + actionBinder.workflowCfg.enabledFeatures = ['mindmap-maker']; + const localStorageStub = sinon.stub(window.localStorage, 'getItem'); + localStorageStub.throws(new Error('localStorage not available')); + + const cOpts = { payload: {} }; + const filesData = { type: 'application/pdf', size: 123, count: 1 }; + sinon.stub(actionBinder, 'getRedirectUrl').resolves(); + actionBinder.redirectUrl = 'https://test-redirect.com'; + + const result = await actionBinder.handleRedirect(cOpts, filesData); + + expect(result).to.be.true; + expect(cOpts.payload.newUser).to.be.true; + expect(cOpts.payload.attempts).to.equal('1st'); + + localStorageStub.restore(); + actionBinder.getRedirectUrl.restore(); + }); }); describe('Experiment Data Integration', () => { diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 90dd8c23..e4343970 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -78,6 +78,7 @@ export default class ActionBinder { 'heic-to-pdf': ['hybrid', 'allowed-filetypes-all', 'allowed-filetypes-heic', 'max-filesize-100-mb'], 'quiz-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], 'flashcard-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], + 'mindmap-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], }; static ERROR_MAP = { diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index e4f1d1c9..2a89e528 100644 --- a/unitylibs/core/workflow/workflow-acrobat/target-config.json +++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json @@ -15,8 +15,8 @@ "sendSplunkAnalytics": true, "verbsWithoutMfuToSfuFallback": ["compress-pdf"], "nonpdfMfuFeedbackScreenTypeNonpdf": ["combine-pdf"], - "nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf", "quiz-maker", "flashcard-maker"], - "mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "quiz-maker", "flashcard-maker"], + "nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf", "quiz-maker", "flashcard-maker", "mindmap-maker"], + "mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "quiz-maker", "flashcard-maker", "mindmap-maker"], "mfuUploadOnlyPdfAllowed": ["combine-pdf"], "experimentationOn": ["add-comment", "png-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "createpdf"], "fetchApiConfig": { diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js index 8c6b9a24..c2328ffa 100644 --- a/unitylibs/core/workflow/workflow.js +++ b/unitylibs/core/workflow/workflow.js @@ -250,6 +250,7 @@ class WfInitiator { 'heic-to-pdf', 'quiz-maker', 'flashcard-maker', + 'mindmap-maker', ]), }, 'workflow-ai': { From 1841b6d7a832c0b6a47ea5150cce83433281ba86 Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Thu, 7 May 2026 12:30:51 +0530 Subject: [PATCH 13/18] browser responsiveness support + few css fixes (#769) Resolves: [MWPW-194198](https://jira.corp.adobe.com/browse/MWPW-194198) [MWPW-194427](https://jira.corp.adobe.com/browse/MWPW-194427) [MWPW-194159](https://jira.corp.adobe.com/browse/MWPW-194159) [MWPW-194450](https://jira.corp.adobe.com/browse/MWPW-194450) **Test URLs:** - Before: https://stage--da-cc--adobecom.aem.page/products/firefly/features/image-to-video?unitylibs=stage - After: https://stage--da-cc--adobecom.aem.page/products/firefly/features/image-to-video?unitylibs=designFixes --------- Co-authored-by: Arushi Gupta --- .../prompt-bar-upload/prompt-bar-upload.css | 260 ++++++++++++++++-- .../prompt-bar-upload/prompt-bar-upload.js | 28 +- .../workflow-prompt-bar-upload/sprite.svg | 8 + 3 files changed, 269 insertions(+), 27 deletions(-) diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css index 9d422f9b..3c4d1752 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css @@ -22,15 +22,19 @@ box-sizing: border-box !important; } @media screen and (min-width: 1200px) { -.unity-prompt-bar-upload.unity-enabled { - max-width: 1000px; -} + .unity-prompt-bar-upload.unity-enabled { + max-width: 1000px; + } -.unity-prompt-bar-upload.unity-enabled .interactive-area { - width: 727px; - max-width: 727px; - padding: 14px !important; -} + .unity-prompt-bar-upload.unity-enabled .interactive-area { + width: 100%; + max-width: 100%; + padding: 14px !important; + } + + .upload-marquee-block .upload-marquee-left.copy { + padding-right: 80px; + } } .ex-unity-wrap.pbu-widget .pbu-legal-foot { @@ -74,7 +78,6 @@ display: flex; flex-direction: column; align-items: flex-start; gap: 17px; -flex-shrink: 0; border-right: 1px solid rgb(255 255 255 / 12%); padding-right: 16px; box-sizing: border-box; @@ -85,7 +88,7 @@ display: flex; flex-direction: column; align-items: stretch; gap: 0; -flex: 1; +flex: 1 1 0%; min-width: 0; } @@ -207,19 +210,21 @@ background-color: #555; .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer { display: flex; flex-direction: row; +flex-wrap: nowrap; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; +min-width: 0; } .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container { display: flex !important; -flex-wrap: wrap; +flex-wrap: nowrap; align-items: center; gap: 8px; justify-content: flex-start; -flex: 1; +flex: 1 1 auto; margin-top: 0 !important; min-width: 0; } @@ -230,7 +235,9 @@ align-items: center; gap: 10px; justify-content: flex-end; margin-top: 0 !important; +flex: 0 0 auto; flex-shrink: 0; +min-width: 0; } .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container:empty ~ .act-wrap { @@ -238,6 +245,191 @@ flex: 1; justify-content: flex-end; } +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg { +display: flex; +align-items: center; +justify-content: center; +flex-shrink: 0; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--layers { +display: none; +} + +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--standard { +display: flex; +} + +@media screen and (max-width: 1799px) { + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model .model-name { + display: none !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-more-btn .btn-txt { + display: none !important; + } + + .unity-prompt-bar-upload.unity-enabled .action-container > a.unity-act-btn.pbu-more-btn.more-btn { + gap: 0; + padding: 6px 10px; + } + + .unity-prompt-bar-upload.unity-enabled .action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico { + padding: 0; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model:hover, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model:hover { + max-width: none; + padding: 6px 10px; + gap: 6px; + background: transparent; + box-shadow: none; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container > a.unity-act-btn.pbu-more-btn.more-btn, + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container > a.unity-act-btn.pbu-more-btn.more-btn:hover { + background: transparent; + box-shadow: none; + } +} + +@media screen and (max-width: 1599px) { + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .model-name { + display: none !important; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .pbu-aspect-models .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .pbu-aspect-models .selected-model:hover, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .pbu-aspect-models .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .pbu-aspect-models .selected-model:hover { + max-width: none; + padding: 6px 10px; + gap: 6px; + background: transparent; + box-shadow: none; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--layers { + display: flex !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--standard { + display: none !important; + } +} + +@media screen and (min-width: 1600px) { + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--layers { + display: none !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--standard { + display: flex !important; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .pbu-aspect-models .selected-model { + max-width: 200px; + padding: 6px 10px; + gap: 8px; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .pbu-aspect-models .selected-model { + padding: 6px 10px; + gap: 8px; + } +} + +@media screen and (min-width: 1800px) { + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-more-btn .btn-txt { + display: block !important; + } + + .unity-prompt-bar-upload.unity-enabled .action-container > a.unity-act-btn.pbu-more-btn.more-btn { + gap: 6px; + } + + .unity-prompt-bar-upload.unity-enabled .action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico { + padding: 6px 0 6px 10px; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model { + max-width: 200px; + padding: 6px 10px; + gap: 8px; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model { + padding: 6px 10px; + gap: 8px; + } +} + +@media screen and (min-width: 600px) and (max-width: 1200px) { + .unity-prompt-bar-upload.unity-enabled .interactive-area .pbu-controls-footer .pbu-aspect-models { + display: flex !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model .model-name { + display: inline !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .model-name { + display: inline !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-more-btn .btn-txt { + display: block !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--standard { + display: flex !important; + } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .pbu-aspect-models .selected-model .pbu-aspect-ratio-icon .pbu-aspect-ratio-svg--layers { + display: none !important; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model:hover, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .pbu-aspect-models .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .pbu-aspect-models .selected-model:hover { + background: #353535; + padding: 6px 10px; + max-width: 200px; + gap: 8px; + box-shadow: none; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model:hover, + .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-controls-footer .pbu-aspect-models .selected-model:hover { + background: #434343; + } + + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .models-container:not(.pbu-aspect-models) .selected-model:hover, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .pbu-aspect-models .selected-model, + .unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-controls-footer .pbu-aspect-models .selected-model:hover { + background: rgb(0 0 0 / 8%); + padding: 6px 10px; + gap: 8px; + box-shadow: none; + } + + .unity-prompt-bar-upload.unity-enabled .action-container > a.unity-act-btn.pbu-more-btn.more-btn { + gap: 6px; + padding: 6px 10px; + background: transparent; + box-shadow: none; + } + + .unity-prompt-bar-upload.unity-enabled .action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico { + padding: 6px 0 6px 10px; + } +} + .unity-prompt-bar-upload.unity-enabled .unity-slf-copy-label { color: #d1d1d1; font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; @@ -263,6 +455,7 @@ color: #f8f8f8 !important; margin-bottom: 18px; font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; font-size: 16px; +padding-top: 10px; } .unity-prompt-bar-upload.unity-enabled .interactive-area.light .inp-field, @@ -360,12 +553,14 @@ display: flex; flex: 0 1 auto; max-width: none; width: auto; +min-width: 0; position: relative; align-items: center; } .unity-prompt-bar-upload.unity-enabled .interactive-area .pbu-controls-footer .models-container { max-width: none; +flex-shrink: 1; } .unity-prompt-bar-upload.unity-enabled .interactive-area.light .verb-list .verb-link, @@ -396,7 +591,7 @@ display: inline-flex; align-items: center; gap: 8px; justify-content: flex-start; -padding: 8px 12px; +padding: 6px 10px; min-height: 32px; width: auto; min-width: 27px; @@ -448,7 +643,7 @@ display: inline-flex; align-items: center; gap: 8px; justify-content: flex-start; -padding: 8px 12px; +padding: 6px 10px; min-height: 40px; width: auto; border: none; @@ -643,7 +838,7 @@ height: 20px; .unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-txt { color: #f8f8f8; -font-size: 15px; +font-size: 14px; font-weight: 400; } @@ -668,6 +863,8 @@ right: 0; .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container { position: relative; z-index: 1; +min-width: 0; +flex-shrink: 1; } .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container.show-menu .verb-list, @@ -678,9 +875,11 @@ animation: none; .pbu-drop-zone-wrap { position: relative; width: 123px; +max-width: 100%; height: 123px; min-height: 123px; flex-shrink: 0; +align-self: flex-start; box-sizing: border-box; } @@ -746,13 +945,12 @@ margin: 0; position: absolute; top: 0; left: 0; -width: 123px; -height: 123px; +width: 100%; +height: 100%; box-sizing: border-box; border-radius: 13.667px; overflow: hidden; border: 2px solid #2680eb; -background: rgb(0 0 0 / 20%); } .pbu-preview.hidden { @@ -761,8 +959,8 @@ display: none; .pbu-preview-img { display: block; -width: 100%; -height: 100%; +width: stretch; +height: stretch; object-fit: cover; border-radius: 13.667px; border: 2px solid #4069FD; @@ -800,8 +998,9 @@ height: 18px; position: absolute; top: 0; left: 0; -width: 123px; -height: 123px; +width: 100%; +height: 100%; +min-height: 0; box-sizing: border-box; border-radius: 13.667px; display: flex; @@ -890,6 +1089,8 @@ height: 20px; } .action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-txt { color: #C6C6C6; +font-size: 14px; +font-weight: 400; } .pbu-main .pbu-controls-footer .act-wrap a.gen-btn { @@ -901,6 +1102,8 @@ gap: 8px; text-decoration: none; display: flex; align-items: center; +white-space: nowrap; +flex-shrink: 0; } .pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-ico { @@ -1003,4 +1206,17 @@ line-height: normal; background: transparent; padding: 4px 6px; } + + .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list, + :root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list { + top: 100%; + margin-top: 38px; + min-width: 320px; + left: -20px; + } + + [dir="rtl"] .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list { + left: auto; + right: -20px; + } } diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js index f9a28fbd..f1beb176 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -39,11 +39,29 @@ function getAspectRatioIconHref(ratio) { const [w, h] = parts; if (w > h) return '#unity-aspect-ratio-horizontal-icon'; if (h > w) return '#unity-aspect-ratio-vertical-icon'; - return '#unity-aspect-ratio-horizontal-icon'; + return '#unity-aspect-ratio-square-icon'; } -function createAspectRatioIconSpan(ratio) { - return createTag('span', { class: 'pbu-aspect-ratio-icon', 'aria-hidden': 'true' }, svgIcon(getAspectRatioIconHref(ratio))); +function createAspectRatioIconSpan(ratio, dualTrigger = false) { + if (!dualTrigger) { + return createTag('span', { class: 'pbu-aspect-ratio-icon', 'aria-hidden': 'true' }, svgIcon(getAspectRatioIconHref(ratio))); + } + const span = createTag('span', { class: 'pbu-aspect-ratio-icon', 'aria-hidden': 'true' }); + const standard = createTag('span', { class: 'pbu-aspect-ratio-svg pbu-aspect-ratio-svg--standard' }); + standard.innerHTML = svgIcon(getAspectRatioIconHref(ratio)); + const layers = createTag('span', { class: 'pbu-aspect-ratio-svg pbu-aspect-ratio-svg--layers' }); + layers.innerHTML = svgIcon('#unity-aspect-ratio-layers-icon'); + span.append(standard, layers); + return span; +} + +function setAspectRatioTriggerIconSvg(triggerAspectIcon, ratio) { + const standard = triggerAspectIcon?.querySelector?.('.pbu-aspect-ratio-svg--standard'); + if (standard) { + standard.innerHTML = svgIcon(getAspectRatioIconHref(ratio)); + } else { + triggerAspectIcon.innerHTML = svgIcon(getAspectRatioIconHref(ratio)); + } } function syncDropdownSelection(list, activeLink) { @@ -368,7 +386,7 @@ export default class PromptBarUploadWidget { if (!ratios.length) return null; this.setSelectedAspectRatio(modelId, ratios[0]); - const triggerAspectIcon = createAspectRatioIconSpan(ratios[0]); + const triggerAspectIcon = createAspectRatioIconSpan(ratios[0], true); const { container, triggerBtn, nameContainer, list } = buildDropdownShell({ label: 'Aspect ratio', menuId: 'pbu-aspect-menu', @@ -399,7 +417,7 @@ export default class PromptBarUploadWidget { e.stopPropagation(); const ratio = link.getAttribute('data-ratio') || ''; nameContainer.textContent = ratio; - triggerAspectIcon.innerHTML = svgIcon(getAspectRatioIconHref(ratio)); + setAspectRatioTriggerIconSvg(triggerAspectIcon, ratio); this.setSelectedAspectRatio(modelId, ratio); syncDropdownSelection(list, link); closeDropdown(container, triggerBtn, list); diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg index 83060d72..d1276aa7 100644 --- a/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg @@ -44,6 +44,14 @@ + + + + + + + + From 739a5f5902880d12bdbcaa4998cf0b1d085b3580 Mon Sep 17 00:00:00 2001 From: Vipul Gupta Date: Thu, 7 May 2026 14:11:15 +0530 Subject: [PATCH 14/18] [MWPW-193434] Doodlebug Wave 2.2 Audio - Adding support for Text to Speech verbs (#767) Adding support for Text to Speech in Unity FE. Pages/Verbs in scope: Text 2 Speech AI Voice Generator AI Character voice AI Male Voice AI Female Voice Epic: https://jira.corp.adobe.com/browse/MWPW-191711 Story: https://jira.corp.adobe.com/browse/MWPW-193434 Design: https://www.figma.com/design/Sk73H9qbSOtphpTJLZ3F6K/Doodlebug-Wave-2---Audio--Video--Image?node-id=1571-29203&p=f&m=dev Authoring Document: https://wiki.corp.adobe.com/spaces/adobedotcom/pages/3872506700/Doodlebug+Wave+2.2+Audio+-+Authoring+Details Resolves: [MWPW-193434](https://jira.corp.adobe.com/browse/MWPW-193434) **Test URLs:** - Before: https://main--da-cc--adobecom.aem.page/drafts/vipulg/doodlebug/text-to-speech/text-to-speech?unitylibs=pb-audio - After: https://main--da-cc--adobecom.aem.page/drafts/vipulg/doodlebug/text-to-speech/text-to-speech?unitylibs=pb-audio1 --------- Co-authored-by: Arushi Gupta Co-authored-by: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Co-authored-by: vipulg --- test/core/workflow/workflow.firefly.test.js | 14 +- .../prompt-bar-audio/prompt-bar-audio.css | 826 ++++++++++++ .../prompt-bar-audio/prompt-bar-audio.js | 1119 +++++++++++++++++ .../prompt-bar-style/prompt-bar-style.js | 12 +- .../prompt-bar-upload/prompt-bar-upload.js | 7 +- .../core/widgets/prompt-bar/prompt-bar.js | 12 +- .../workflow-firefly/action-binder.js | 44 +- .../core/workflow/workflow-firefly/sprite.svg | 13 + .../workflow-firefly/target-config.json | 7 + unitylibs/core/workflow/workflow.js | 10 +- unitylibs/scripts/utils.js | 8 + 11 files changed, 2038 insertions(+), 34 deletions(-) create mode 100644 unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css create mode 100644 unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js diff --git a/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index 3f0ddd66..d86a0ea1 100644 --- a/test/core/workflow/workflow.firefly.test.js +++ b/test/core/workflow/workflow.firefly.test.js @@ -26,7 +26,13 @@ describe('Firefly Workflow Tests', () => { unityElement = document.querySelector('.unity'); workflowCfg = { name: 'workflow-firefly', - targetCfg: { renderWidget: true, insert: 'before', target: 'a:last-of-type' }, + targetCfg: { + renderWidget: true, + insert: 'before', + target: 'a:last-of-type', + limits: { 'max-char-limit': 750 }, + 'limits-prompt-bar-audio': { 'max-char-limit': 5000 }, + }, }; spriteContainer = ''; block = document.querySelector('.unity-enabled'); @@ -2279,9 +2285,7 @@ describe('Firefly Workflow Tests', () => { }); it('should pass empty object to logAnalytics when splunkData is omitted', async () => { - await testActionBinder.sendFireflyAnalytics(new CustomEvent('firefly-analytics', { - detail: { adobeEventName: 'Enter Prompt|UnityWidget' }, - })); + await testActionBinder.sendFireflyAnalytics(new CustomEvent('firefly-analytics', { detail: { adobeEventName: 'Enter Prompt|UnityWidget' } })); expect(logStub.calledOnceWithExactly('Enter Prompt|UnityWidget', {})).to.be.true; }); }); @@ -2758,7 +2762,7 @@ describe('Firefly Workflow Tests', () => { { label: 'Var A', url: 'https://u1' }, { label: 'Var B', url: 'https://u2' }, { label: 'Var C', url: 'https://u3' }, - { label: 'Var D', url: 'https://u4' } + { label: 'Var D', url: 'https://u4' }, ]); expect(pm.image).to.exist; expect(pm.image[0].variations).to.deep.equal([]); diff --git a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css new file mode 100644 index 00000000..8bbae74d --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css @@ -0,0 +1,826 @@ +.unity-slf-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.unity.workflow-firefly.widget-prompt-bar-audio.unity-prompt-bar-audio-host { + position: relative; + min-height: 0; +} + +.unity-prompt-bar-audio { + width: 100%; + box-sizing: border-box; +} + +.unity-prompt-bar-audio .unity-paf-main { + display: flex; + flex-direction: column; + gap: 0; + align-items: stretch; + flex: 1; + min-width: 0; +} + +.unity-prompt-bar-audio .unity-slf-left { + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; + flex: 1; + background: #1b1b1b; + border-radius: 10px; + padding: 16px; + box-sizing: border-box; +} + +.unity-prompt-bar-audio .unity-slf-controls { + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; +} + +.unity-prompt-bar-audio .unity-paf-voice-section { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.unity-prompt-bar-audio .unity-paf-voice-heading { + margin: 18px 0 0; + padding-top: 16px; + border-top: 2px solid rgb(255 255 255 / 12%); +} + +.unity-prompt-bar-audio .unity-paf-voice-row { + display: flex; + flex-flow: row nowrap; + gap: 4.76px; + align-items: stretch; + justify-content: flex-start; + width: 100%; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; + padding-bottom: 2px; +} + +.unity-prompt-bar-audio .unity-paf-voice-row.unity-paf-voice-row-peek { + box-sizing: border-box; + width: calc(100% + 16px); + padding-right: 16px; + scroll-padding-right: 16px; +} + +.unity-prompt-bar-audio .unity-paf-voice-row.unity-paf-voice-row-peek.unity-paf-voice-row-peek-scrolled { + width: calc(100% + 32px); + transform: translateX(-16px); + padding-left: 16px; + padding-right: 16px; + scroll-padding-left: 16px; + scroll-padding-right: 16px; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile { + --unity-paf-voice-tile-width: 231px; + --paf-voice-tile-border-width: 1.78px; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 5px; + box-sizing: border-box; + width: var(--unity-paf-voice-tile-width); + min-width: var(--unity-paf-voice-tile-width); + max-width: var(--unity-paf-voice-tile-width); + flex: 0 0 var(--unity-paf-voice-tile-width); + padding: 14px; + border-radius: 9.51px; + border: var(--paf-voice-tile-border-width) solid transparent; + background: #292929; + backdrop-filter: blur(29.72px); + overflow: hidden; + cursor: pointer; + text-align: start; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:hover { + background: #333; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus { + outline: none; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible { + border-color: #274dea; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile.selected { + border-color: #274dea; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile.selected:hover { + background: #2f2f2f; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile-text { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; + flex: 1 1 auto; +} + +.unity-prompt-bar-audio .unity-paf-voice-name { + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 16px; + font-weight: 800; + line-height: 0.98; + letter-spacing: -0.48px; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.unity-prompt-bar-audio .unity-paf-voice-desc { + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 1.2; + letter-spacing: 0.14px; + color: rgb(255 255 255 / 92%); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.unity-prompt-bar-audio .unity-paf-voice-player { + position: relative; + width: 37.17px; + height: 37.17px; + flex: 0 0 37.17px; +} + +@media (hover: hover) and (pointer: fine) { + .unity-prompt-bar-audio .unity-paf-voice-tile .unity-paf-voice-player { + opacity: 0; + transition: opacity 0.15s ease; + } + + .unity-prompt-bar-audio .unity-paf-voice-tile:hover .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile[aria-pressed="true"] .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile.selected .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile:has(.unity-paf-voice-player--buffering) .unity-paf-voice-player { + opacity: 1; + } +} + +.unity-prompt-bar-audio .unity-paf-progress-svg { + display: block; + width: 37.17px; + height: 37.17px; +} + +.unity-prompt-bar-audio .unity-paf-ring-bg { + stroke: rgb(255 255 255 / 22%); +} + +.unity-prompt-bar-audio .unity-paf-ring-fg { + stroke: #f8f8f8; + transition: stroke-dashoffset 0.05s linear; +} + +.unity-prompt-bar-audio .unity-paf-pp-center { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.unity-prompt-bar-audio .unity-paf-pp-center .unity-paf-pp-svg { + display: block; + width: 18px; + height: 18px; +} + +.unity-prompt-bar-audio .unity-paf-pp-center .unity-paf-pp-svg use { + filter: brightness(0) invert(1); +} + +.unity-prompt-bar-audio .unity-paf-voice-player-loading { + box-sizing: border-box; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + color: #f8f8f8; + pointer-events: none; +} + +.unity-prompt-bar-audio .unity-paf-voice-player-loading-svg { + display: block; + width: 100%; + height: 100%; +} + +.unity-prompt-bar-audio .unity-paf-voice-player-loading-circle { + stroke-dasharray: 125.664; + stroke-dashoffset: 125.664; + animation: unity-paf-voice-player-loading-sweep 1s ease-in-out infinite; +} + +@keyframes unity-paf-voice-player-loading-sweep { + to { + stroke-dashoffset: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .unity-prompt-bar-audio .unity-paf-voice-player-loading-circle { + animation: none; + stroke-dashoffset: 62.8; + } +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + margin: 4px 0 0; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 12px; + line-height: 16px; + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot-line { + margin: 0; + max-width: 100%; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner { + box-sizing: border-box; + width: 100%; + flex-shrink: 0; + margin: 24px 0 0; + padding: 0 8px; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 12px; + line-height: 18px; + color: #fff; +} + +.unity-prompt-bar-audio:has(.interactive-area.light) .unity-paf-terms-banner { + color: rgb(46 46 46 / 92%); +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line { + margin: 0 auto; + max-width: 56rem; +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot-line a, +.unity-prompt-bar-audio .unity-paf-voice-footer-link { + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line a { + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line a:hover { + color: inherit; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line a:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + border-radius: 2px; +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot-line a:hover { + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-footer { + margin: 4px 0 0; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 12px; + line-height: 16px; + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-footer-link:hover { + color: #fff; +} + +.unity-prompt-bar-audio .unity-slf-sprite { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + pointer-events: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-field { + border: none; + width: 100%; + box-sizing: border-box; + font-family: inherit; + outline: none; + background: transparent; + resize: none; + margin: 0; + scrollbar-width: thin; + scrollbar-color: #888 transparent; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-field::placeholder { + color: #7d7d7d; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .models-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .verbs-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .action-container { + position: relative; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb { + display: flex; + align-items: center; + gap: 7px; + justify-content: flex-start; + padding: 7px 12px; + cursor: pointer; + border: none; + font-size: 14px; + font-family: inherit; + font-weight: 400; + width: 100%; + min-width: unset; + background: #2c2c2c; + border-radius: 8px; + color: #fff; + height: 32px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model { + padding-left: 4px; + padding-right: 11px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .model-name { + flex: 0 1 auto; + min-width: min-content; + overflow: visible; + text-overflow: clip; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model img, +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link img { + width: 20px; + height: 20px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon { + font-size: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon svg, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon svg { + width: 12px; + height: 12px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon svg, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon svg { + filter: invert(1); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link .selected-icon { + font-size: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link .selected-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link .selected-icon svg { + width: 12px; + height: 12px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container .menu-icon { + position: relative; + top: 1px; + flex-shrink: 0; + transition: transform 0.15s ease-in; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container.show-menu .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container.show-menu .menu-icon { + transform: rotate(-180deg); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container .verb-list .verb-link:not(.model-link) img { + filter: invert(1); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .models-container .model-name { + color: #fff; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .models-container .verb-list, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .verbs-container .verb-list { + background: #000; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list { + padding: 18px; + list-style: none; + box-shadow: 0 0 10px #0000001c; + border-radius: 10px; + color: #fff; + margin: 0; + animation: none; + position: absolute; + top: 0; + left: 0; + z-index: 5; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container.show-menu .verb-list, +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container.show-menu .verb-list { + display: block; + animation: none; + transform: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container .verb-list { + padding: 10px 12px; + min-height: 0; + box-sizing: border-box; + min-width: 100%; + width: max-content; + max-width: 100vw; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item { + margin: 0; + padding: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link { + color: inherit; + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + padding-inline-start: 25px; + text-transform: capitalize; + text-decoration: none; + text-align: start; + position: relative; + opacity: 1; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link.model-link { + padding: 8px 12px 8px 30px; + gap: 10px; + line-height: 1.25; + font-size: 14px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link.model-link img { + object-fit: contain; + flex-shrink: 0; + align-self: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link.model-link .model-name { + align-self: center; + line-height: 20px; + white-space: nowrap; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item .selected-icon { + display: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item.selected .verb-link .selected-icon { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 8px; + top: 0; + bottom: 0; + margin-block: auto; + width: 14px; + height: 14px; + transform: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item.selected .verb-link .selected-icon svg { + display: block; +} + +[dir="rtl"] .unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item.selected .verb-link .selected-icon { + left: auto; + right: 8px; +} + +.unity-prompt-bar-audio .unity-slf-copy-label { + color: #d1d1d1; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; + letter-spacing: 0; +} + +.unity-prompt-bar-audio .unity-slf-prompt-label { + display: block; + margin-bottom: 8px; +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area.dark .unity-slf-prompt-label { + margin-bottom: 0; +} + +.unity-prompt-bar-audio .inp-field { + min-height: 100px; + resize: vertical; +} + +.unity-prompt-bar-audio .unity-slf-gen-btn { + white-space: nowrap; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap { + display: flex; + width: fit-content; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn { + text-decoration: none; + display: flex; + align-items: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn:focus { + outline: 2px solid #005fcc; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn .btn-ico { + display: flex; + align-content: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn .btn-ico img { + width: 22px; + height: 22px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn .btn-txt { + display: flex; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .gen-btn { + flex-shrink: 0; + border-radius: 25px; + background: linear-gradient(90deg, #d73220 0%, #d92361 33%, #7155fa 100%); + border: none; + padding: 10px 20px 10px 18px; + gap: 8px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .gen-btn .btn-txt { + color: var(--color-white); + font-size: 16px; + font-weight: 700; + line-height: normal; +} + +.unity-prompt-bar-audio.unity-enabled { + margin-inline: auto; + padding-bottom: 76px; +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area { + display: flex; + box-sizing: border-box; + padding: 14px; + border-radius: 20px; + gap: 8px; + isolation: isolate; + overflow-x: clip; +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area.dark { + background: rgb(24 24 24 / 48%); + backdrop-filter: blur(32px) saturate(165%); + -webkit-backdrop-filter: blur(32px) saturate(165%); + border: 1px solid rgb(255 255 255 / 14%); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 12%), + 0 16px 48px rgb(0 0 0 / 28%); +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area.light { + background: rgb(255 255 255 / 52%); + backdrop-filter: blur(32px) saturate(165%); + -webkit-backdrop-filter: blur(32px) saturate(165%); + border: 1px solid rgb(0 0 0 / 8%); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 85%), + 0 16px 48px rgb(0 0 0 / 10%); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area.dark .unity-slf-left { + background: rgb(255 255 255 / 5%); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area.light .unity-slf-left { + background: rgb(0 0 0 / 4%); +} + +@media (prefers-reduced-transparency: reduce) { + .unity-prompt-bar-audio.unity-enabled > .interactive-area.dark { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: #222; + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: none; + } + + .unity-prompt-bar-audio.unity-enabled > .interactive-area.light { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: #eaeaea; + border: 1px solid rgb(0 0 0 / 10%); + box-shadow: none; + } + + .unity-prompt-bar-audio.unity-enabled .interactive-area.dark .unity-slf-left { + background: #1b1b1b; + } + + .unity-prompt-bar-audio.unity-enabled .interactive-area.light .unity-slf-left { + background: #fff; + } +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .autocomplete { + background: transparent !important; + border: none !important; + border-radius: 0 !important; + width: 100% !important; + max-width: 100%; + margin: 0 !important; + box-sizing: border-box; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .autocomplete::after { + content: none !important; + display: none !important; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap:not(.sticky) .ex-unity-widget { + padding-left: 0; + padding-right: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap { + display: grid; + grid-template-columns: 1fr auto; + column-gap: 8px; + align-items: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .unity-slf-copy-label, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > label { + grid-column: 1 / -1; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .inp-field { + grid-column: 1 / -1; + color: #f8f8f8; + padding: 10px 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container { + grid-column: 1; + margin-top: 32px; + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-start; + min-width: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .act-wrap { + grid-column: 2; + margin-top: 24px; + display: flex; + justify-content: flex-end; + align-items: center; + align-self: center; + min-width: max-content; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container:empty { + display: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container:empty + .act-wrap { + grid-column: 1 / -1; + justify-self: end; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .models-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .verbs-container { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .models-container { + min-width: 0; + max-width: 100%; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container { + display: flex; + width: 100%; + max-width: 100%; + position: relative; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .models-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .verbs-container { + width: fit-content; +} + +@media screen and (max-width: 1199px) { + .unity-prompt-bar-audio.unity-enabled { + width: var(--grid-container-width); + } + + .unity-prompt-bar-audio .unity-paf-voice-row { + gap: 10px; + } + + .unity-prompt-bar-audio .unity-paf-voice-tile { + --unity-paf-voice-tile-width: 201px; + } +} + +@media screen and (min-width: 1200px) { + .unity-prompt-bar-audio.unity-enabled { + max-width: 1000px; + } + + .hero-marquee:has(+ .unity-prompt-bar-audio) > .foreground { + max-width: 1000px; + min-width: unset; + } + + .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .main-copy { + align-items: center; + } + + .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .main-copy p { + max-width: 800px; + } +} diff --git a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js new file mode 100644 index 00000000..0f09c7ab --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js @@ -0,0 +1,1119 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ + +import { createTag, getConfig, getUnityPromptConfigsBaseUrl } from '../../../scripts/utils.js'; + +let promptWithStyleEvents = null; + +function buildVoiceModelIndex(voices) { + const indexMap = new Map(); + voices.forEach((voice) => { + const modelId = `${voice?.modelId ?? ''}`.trim().toLowerCase(); + if (!indexMap.has(modelId)) indexMap.set(modelId, []); + indexMap.get(modelId).push(voice); + }); + return indexMap; +} + +function filterVoicesByModelId(voices, voiceModelIndex, selectedModelId) { + const id = (selectedModelId || '').trim().toLowerCase(); + if (!id) return voices; + const shared = voiceModelIndex?.get('') || []; + const specific = voiceModelIndex?.get(id) || []; + return [...shared, ...specific]; +} + +async function loadVoicesFromCurrentPageJson(sourceUrl) { + const finalUrl = sourceUrl?.trim(); + if (!finalUrl) return []; + const res = await fetch(finalUrl); + if (!res.ok) { + throw new Error(`Current page config fetch failed: ${res.status}`); + } + const json = await res.json(); + const data = json?.content?.data; + let rows = []; + if (Array.isArray(data)) { + rows = data; + } else if (data && typeof data === 'object' && Array.isArray(data.voices)) { + rows = data.voices; + } + if (!rows.length) return []; + return rows + .filter((row) => row && typeof row === 'object') + .map((row) => ({ + name: `${row.Name ?? ''}`.trim(), + description: `${row.Description ?? ''}`.trim(), + url: `${row.url ?? ''}`.trim(), + voiceId: `${row.VoiceId ?? ''}`.trim(), + modelId: `${row.ModelId ?? ''}`.trim(), + })); +} + +function resolveCurrentPageVariationFileUrl(root) { + const el = root?.querySelector?.('[class*="icon-operation-"]'); + const token = el && [...el.classList].find((c) => c.startsWith('icon-operation-')); + const rawSuffix = token ? token.slice('icon-operation-'.length) : ''; + const normalized = rawSuffix.trim().replace(/\s+/g, '-'); + const base = normalized.replace(/[^a-zA-Z0-9._-]/g, ''); + const fileBase = base ? base.toLowerCase() : null; + if (!fileBase) return null; + const baseUrl = getUnityPromptConfigsBaseUrl(); + const { locale } = getConfig(); + return locale.prefix && locale.prefix !== '/' + ? `${baseUrl}${locale.prefix}/unity/configs/prompt/${fileBase}.json` + : `${baseUrl}/unity/configs/prompt/${fileBase}.json`; +} + +class UnityWidget { + constructor(target, el, workflowCfg, spriteCon) { + this.el = el; + this.target = target; + this.workflowCfg = workflowCfg; + this.widget = null; + this.actionMap = {}; + this.spriteCon = spriteCon; + this.prompts = null; + this.models = null; + this.selectedVerbType = ''; + this.selectedVerbText = ''; + this.selectedModelModule = ''; + this.selectedModelId = ''; + this.selectedModelText = ''; + this.selectedModelVersion = ''; + this.selectedModelName = ''; + this.promptItems = []; + this.genBtn = null; + this.hasPromptSuggestions = false; + this.hasModelOptions = false; + this.voices = null; + this.voiceConfigAll = null; + this.voiceModelIndex = null; + this.lanaOptions = { sampleRate: 100, tags: 'Unity-FF' }; + this.sound = { audio: null, currentTile: null, currentUrl: '' }; + this.durationCache = new Map(); + } + + verbDropdown() { + const verb = this.el.querySelector('[class*="icon-verb"]'); + const selectedVerb = verb?.nextElementSibling; + this.selectedVerbType = verb?.className.split('-')[2]; + this.selectedVerbText = selectedVerb?.textContent.trim() ?? ''; + this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType); + } + + closeVerbOrModelMenu(selectedElement) { + const menuContainer = selectedElement?.parentElement; + if (!menuContainer) return; + menuContainer.classList.remove('show-menu'); + selectedElement.setAttribute('aria-expanded', 'false'); + const list = selectedElement.nextElementSibling; + if (list?.classList?.contains('verb-list')) { + list.setAttribute('style', 'display: none;'); + } + } + + showVerbMenu(selectedElement) { + const menuContainer = selectedElement.parentElement; + document.querySelectorAll('.models-container').forEach((container) => { + if (container !== menuContainer) { + const sm = container.querySelector('.selected-model'); + if (sm) this.closeVerbOrModelMenu(sm); + } + }); + menuContainer.classList.toggle('show-menu'); + const isOpen = menuContainer.classList.contains('show-menu'); + selectedElement.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + const siblingList = selectedElement.nextElementSibling; + if (siblingList?.classList?.contains('verb-list')) { + if (isOpen) { + siblingList.removeAttribute('style'); + } else { + siblingList.setAttribute('style', 'display: none;'); + } + } + } + + showVerbOrModelMenuAndTrackOpen(selectedElement, adobeEventName) { + const menuContainer = selectedElement.parentElement; + const wasOpen = menuContainer.classList.contains('show-menu'); + this.hidePromptDropdown(selectedElement); + this.showVerbMenu(selectedElement); + if (!wasOpen) { + this.widgetWrap.dispatchEvent(new CustomEvent('firefly-analytics', { + detail: { + adobeEventName, + splunkData: { action: 'open' }, + }, + })); + } + } + + hidePromptDropdown(exceptElement = null) { + const dropdown = this.widget.querySelector('.prompt-dropdown-container'); + if (dropdown && !dropdown.classList.contains('hidden')) { + dropdown.classList.add('hidden'); + dropdown.setAttribute('inert', ''); + dropdown.setAttribute('aria-hidden', 'true'); + } + if (this.selectedVerbType === 'sound') { + this.resetAllSoundVariations?.(dropdown); + } + const modelDropdown = this.widget.querySelector('.models-container'); + const modelButton = modelDropdown?.querySelector('.selected-model'); + if (modelDropdown && modelDropdown.classList.contains('show-menu') && modelButton && modelButton !== exceptElement) { + this.closeVerbOrModelMenu(modelButton); + } + } + + updateAnalytics(verb) { + if (this.promptItems && this.promptItems.length > 0) { + this.promptItems.forEach((item) => { + const ariaLabel = item.getAttribute('aria-label') || ''; + item.setAttribute('daa-ll', `${ariaLabel.slice(0, 20)}--${verb}--Prompt suggestion`); + }); + } + } + + clearSelectedModelState() { + this.selectedModelId = ''; + this.selectedModelName = ''; + this.selectedModelVersion = ''; + this.selectedModelModule = ''; + this.selectedModelText = ''; + this.widgetWrap?.removeAttribute('data-selected-model-name'); + } + + handleModelLinkClick(link, listContainer, selectedElement, menuIcon) { + return (e) => { + e.preventDefault(); + e.stopPropagation(); + const verbLinkTexts = []; + listContainer.querySelectorAll('.verb-link').forEach((listLink) => { + listLink.parentElement.classList.remove('selected'); + listLink.setAttribute('aria-selected', 'false'); + const text = listLink.textContent.trim(); + if (text) verbLinkTexts.push(text); + }); + verbLinkTexts.sort((a, b) => b.length - a.length); + this.closeVerbOrModelMenu(selectedElement); + link.parentElement.classList.add('selected'); + link.setAttribute('aria-selected', 'true'); + this.selectedModelId = link.getAttribute('data-model-id'); + this.selectedModelName = link.textContent.trim(); + this.selectedModelVersion = link.getAttribute('data-model-version'); + this.selectedModelModule = link.getAttribute('data-model-module'); + this.selectedModelText = link.textContent.trim(); + const copiedNodes = link.cloneNode(true).childNodes; + copiedNodes[0].remove(); + selectedElement.replaceChildren(...copiedNodes, menuIcon); + selectedElement.dataset.selectedModelId = this.selectedModelId; + selectedElement.dataset.selectedModelVersion = this.selectedModelVersion; + selectedElement.focus(); + const verbsWithoutPromptSuggestions = this.workflowCfg.targetCfg?.verbsWithoutPromptSuggestions ?? []; + if (verbsWithoutPromptSuggestions.includes(this.selectedVerbType)) { + this.widgetWrap.dispatchEvent(new CustomEvent('firefly-reinit-action-listeners')); + } + if (link.getAttribute('data-model-module') !== this.selectedVerbType) { + const oldModelContainer = this.widget.querySelector('.models-container'); + const modelDropdown = this.modelDropdown(); + if (oldModelContainer) { + if (modelDropdown.length > 1) { + const newModelContainer = createTag('div', { class: 'models-container', 'aria-label': 'Model options' }); + newModelContainer.append(...modelDropdown); + oldModelContainer.replaceWith(newModelContainer); + } else { + oldModelContainer.remove(); + this.clearSelectedModelState(); + } + } else if (modelDropdown.length > 1) { + const actionContainer = this.widget.querySelector('.action-container'); + if (actionContainer) { + const newModelContainer = createTag('div', { class: 'models-container', 'aria-label': 'Prompt options' }); + newModelContainer.append(...modelDropdown); + actionContainer.append(newModelContainer); + } + } else this.clearSelectedModelState(); + } + this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType); + if (this.selectedModelId) { + this.widgetWrap.setAttribute('data-selected-model-id', this.selectedModelId); + this.widgetWrap.setAttribute('data-selected-model-name', this.selectedModelName || ''); + } else { + this.widgetWrap.removeAttribute('data-selected-model-id'); + this.widgetWrap.removeAttribute('data-selected-model-name'); + } + if (this.selectedModelVersion) this.widgetWrap.setAttribute('data-selected-model-version', this.selectedModelVersion); + else this.widgetWrap.removeAttribute('data-selected-model-version'); + this.updateAnalytics(this.selectedVerbType); + if (typeof this.refreshVoiceTilesForModel === 'function') { + this.refreshVoiceTilesForModel(); + } + if (this.genBtn) { + const img = this.genBtn.querySelector('img[src*=".svg"]'); + this.genBtn.setAttribute( + 'aria-label', + (this.genBtn.getAttribute('aria-label') || '').replace( + new RegExp(`\\b(${verbLinkTexts.join('|')})\\b`), + this.selectedVerbText, + ), + ); + if (img) img.setAttribute('alt', `${this.genBtn.getAttribute('aria-label') || ''}`); + } + }; + } + + createDropdownItems(items, listContainer, selectedElement, menuIcon) { + const fragment = document.createDocumentFragment(); + items.forEach((item, idx) => { + const { name, icon, module, id, version } = item; + const listItem = createTag('li', { + class: 'verb-item', + role: 'presentation', + }); + const selectedIcon = createTag('span', { class: 'selected-icon' }, ''); + const nameContainer = createTag('span', { class: 'model-name' }, name.trim()); + const link = createTag('a', { + href: '#', + class: 'verb-link model-link', + 'data-model-module': module, + 'data-model-id': id, + 'data-model-version': version, + 'aria-selected': 'false', + role: 'option', + }, `${nameContainer.outerHTML}`); + if (idx === 0) { + listItem.classList.add('selected'); + link.setAttribute('aria-selected', 'true'); + } + link.prepend(selectedIcon); + listItem.append(link); + fragment.append(listItem); + }); + listContainer.append(fragment); + listContainer.addEventListener('click', (e) => { + const link = e.target.closest('.verb-link'); + if (!link) return; + this.handleModelLinkClick(link, listContainer, selectedElement, menuIcon)(e); + }); + listContainer.addEventListener('keydown', (e) => { + if (e.key !== 'Tab') return; + const menuContainer = selectedElement.parentElement; + if (!menuContainer?.classList.contains('show-menu')) return; + const links = listContainer.querySelectorAll('.verb-link'); + if (!links.length) return; + const active = document.activeElement; + const idx = [...links].findIndex((a) => a === active || a.contains(active)); + if (idx < 0) return; + const atStart = idx === 0; + const atEnd = idx === links.length - 1; + if ((e.shiftKey && atStart) || (!e.shiftKey && atEnd)) { + this.closeVerbOrModelMenu(selectedElement); + } + }); + } + + modelDropdown() { + if (!this.hasModelOptions) return []; + const models = Array.isArray(this.models) + ? this.models.filter((obj) => obj.module === this.selectedVerbType) + : []; + if (models.length === 0) return []; + const selectedModelType = models[0].id; + const selectedModelVersion = models[0].version; + const selectedModelModule = models[0].module; + const selectedModelName = models[0].name.trim(); + const nameContainer = createTag('span', { class: 'model-name' }, models[0].name.trim()); + const selectedElement = createTag('button', { + class: 'selected-model', + 'aria-expanded': 'false', + 'aria-controls': 'model-menu', + 'aria-label': 'model type', + 'aria-haspopup': 'listbox', + role: 'combobox', + 'aria-labelledby': 'listbox-label', + 'data-selected-model-id': selectedModelType, + 'data-selected-model-version': selectedModelVersion, + 'data-selected-model-module': selectedModelModule, + }, `${nameContainer.outerHTML}`); + this.selectedModelModule = selectedModelModule; + this.selectedModelId = selectedModelType; + this.selectedModelVersion = selectedModelVersion; + this.selectedModelName = selectedModelName; + this.widgetWrap.setAttribute('data-selected-model-id', this.selectedModelId); + this.widgetWrap.setAttribute('data-selected-model-version', this.selectedModelVersion); + this.widgetWrap.setAttribute('data-selected-model-name', this.selectedModelName); + this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType); + this.selectedModelText = models[0].name.trim(); + const menuIcon = createTag('span', { class: 'menu-icon' }, ''); + const listItems = createTag('ul', { class: 'verb-list', id: 'model-menu', role: 'listbox', 'aria-labelledby': 'listbox-label' }); + listItems.setAttribute('style', 'display: none;'); + selectedElement.append(menuIcon); + const handleDocumentClick = (e) => { + const menuContainer = selectedElement.parentElement; + if (!menuContainer.contains(e.target)) { + document.removeEventListener('click', handleDocumentClick); + this.closeVerbOrModelMenu(selectedElement); + } + }; + selectedElement.addEventListener('click', (e) => { + e.stopPropagation(); + this.showVerbOrModelMenuAndTrackOpen(selectedElement, promptWithStyleEvents.MODEL_SELECT_DROPDOWN); + document.addEventListener('click', handleDocumentClick); + }, true); + selectedElement.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + this.showVerbOrModelMenuAndTrackOpen(selectedElement, promptWithStyleEvents.MODEL_SELECT_DROPDOWN); + } + if (e.key === 'Escape') { + this.closeVerbOrModelMenu(selectedElement); + selectedElement.focus(); + } + }); + this.createDropdownItems(models, listItems, selectedElement, menuIcon); + return [selectedElement, listItems]; + } + + createActBtn(cfg, cls) { + if (!cfg) return null; + const txt = cfg.innerText?.trim(); + const img = cfg.querySelector('img[src*=".svg"]'); + if (img) img.setAttribute('alt', `${txt?.split('\n')[0]} ${this.selectedVerbText}`); + const btn = createTag('a', { href: '#', class: `unity-act-btn ${cls}`, 'daa-ll': promptWithStyleEvents.GENERATE_CTA, 'aria-label': `${txt?.split('\n')[0]} ${this.selectedVerbText}` }); + if (img) btn.append(createTag('div', { class: 'btn-ico' }, img)); + if (txt) btn.append(createTag('div', { class: 'btn-txt' }, txt.split('\n')[0])); + this.genBtn = btn; + return btn; + } + + async loadModels() { + const modelFile = `${getUnityPromptConfigsBaseUrl()}/unity/configs/prompt/model-picker.json`; + const results = await fetch(modelFile); + if (!results.ok) { + throw new Error('Failed to fetch models.'); + } + const modelJson = await results.json(); + this.models = modelJson?.content?.data; + } + + async getModel() { + if (!this.hasModelOptions) return []; + try { + if (!this.models || Object.keys(this.models).length === 0) await this.loadModels(); + return this.models; + } catch (e) { + window.lana?.log(`Message: Error loading models, Error: ${e}`, this.lanaOptions); + return []; + } + } +} + +const RING_C = 2 * Math.PI * 20; +const RING_STROKE_ATTR = `${(2.78751 * 48) / 33}`; +const PAF_PP_PLAY_SVG = ''; +const PAF_PP_PAUSE_SVG = ''; +const PAF_PROGRESS_SVG = ``; +const PAF_PLAYER_LOADING_SVG = ``; +const voiceTileState = new WeakMap(); + +function setVoiceTilePlayerBuffering(tile, isBuffering) { + const p = voiceTileState.get(tile); + if (!p?.player || !p.bufferLayer || !p.progressSvg || !p.center) return; + const on = Boolean(isBuffering); + if (on === p.bufferingUi) return; + p.bufferingUi = on; + if (on) { + p.player.classList.add('unity-paf-voice-player--buffering'); + p.bufferLayer.setAttribute('aria-busy', 'true'); + p.player.replaceChildren(p.bufferLayer); + } else { + p.player.classList.remove('unity-paf-voice-player--buffering'); + p.bufferLayer.removeAttribute('aria-busy'); + p.player.replaceChildren(p.progressSvg, p.center); + } +} + +function setVoiceTileCenterIcon(tile, iconSvg) { + const p = voiceTileState.get(tile); + if (!p?.center) return; + setVoiceTilePlayerBuffering(tile, false); + p.center.innerHTML = iconSvg; +} + +function primeVoiceAudioForPlayback(tile) { + const p = voiceTileState.get(tile); + if (!p || p.audio.src) return; + setVoiceTilePlayerBuffering(tile, true); + p.audio.preload = 'auto'; + p.audio.src = p.url; +} + +function findPlaceholderIconLi(root, iconClass) { + const icon = root.querySelector(`.${iconClass}`) + || root.querySelector(`[class*="${iconClass}"]`); + return icon?.closest('li') ?? null; +} + +function placeholderRowText(root, iconClass) { + const li = findPlaceholderIconLi(root, iconClass); + if (!li) return ''; + return (li.innerText || '').replace(/\s+/g, ' ').trim(); +} + +function placeholderRowHtmlAfterIcon(root, iconClass) { + const li = findPlaceholderIconLi(root, iconClass); + if (!li) return ''; + const clone = li.cloneNode(true); + const rm = clone.querySelector(`.${iconClass}`) || clone.querySelector(`[class*="${iconClass}"]`); + if (rm) rm.remove(); + return (clone.innerHTML || '').replace(/^\s+/, '').trim(); +} + +function findFooterLinkInRoot(root) { + const anchors = Array.from(root.querySelectorAll('a[href^="https://"]')); + const a = anchors.find((el) => { + const href = el.getAttribute('href')?.trim() ?? ''; + if (!href) return false; + try { + if (/\.json$/i.test(new URL(href, window.location.href).pathname)) return false; + } catch { + return false; + } + return true; + }); + if (!a) return null; + const href = a.getAttribute('href')?.trim() ?? ''; + return { href, text: a.textContent?.trim() || href }; +} + +function dispatchAudioPlaybackFailed(widgetWrap) { + try { + widgetWrap?.dispatchEvent(new CustomEvent('firefly-audio-error', { detail: { error: 'audio-playback-failed' } })); + } catch (e) { + window.lana?.log(`Message: Error dispatching audio playback failed event, Error: ${e}`, this.lanaOptions); + } +} + +export function parsePromptBarAudioAuthoring(root) { + return { + footerLink: findFooterLinkInRoot(root), + sectionHeading: placeholderRowText(root, 'icon-placeholder-voice') || 'Choose a voice', + currentPageSourceUrl: resolveCurrentPageVariationFileUrl(root), + defaultPrompt: placeholderRowText(root, 'placeholder-prompt-default') || '', + exploreHtml: placeholderRowHtmlAfterIcon(root, 'placeholder-explore'), + termsHtml: placeholderRowHtmlAfterIcon(root, 'placeholder-terms'), + }; +} + +function buildVoiceTile(voice, index, row, widgetInstance) { + const { name, description, url, voiceId } = voice; + const tile = createTag('div', { + class: `unity-paf-voice-tile${index === 0 ? ' selected' : ''}`, + role: 'listitem', + tabindex: '0', + 'aria-pressed': 'false', + 'data-voice-index': String(index), + 'data-voice-name': name, + }); + if (voiceId) tile.setAttribute('data-voice-id', voiceId); + if (index === 0) tile.setAttribute('aria-current', 'true'); + + const textCol = createTag('div', { class: 'unity-paf-voice-tile-text' }); + textCol.append( + createTag('span', { class: 'unity-paf-voice-name' }, name), + createTag('span', { class: 'unity-paf-voice-desc' }, description), + ); + + const player = createTag('div', { class: 'unity-paf-voice-player' }); + player.insertAdjacentHTML('beforeend', PAF_PROGRESS_SVG); + const progressSvg = player.querySelector('.unity-paf-progress-svg'); + const ringFg = progressSvg?.querySelector('.unity-paf-ring-fg'); + if (!progressSvg || !ringFg) return tile; + ringFg.style.strokeDasharray = String(RING_C); + ringFg.style.strokeDashoffset = String(RING_C); + const center = createTag('div', { class: 'unity-paf-pp-center' }); + center.innerHTML = PAF_PP_PLAY_SVG; + const bufferLayer = createTag('div', { class: 'unity-paf-voice-player-loading' }); + bufferLayer.innerHTML = PAF_PLAYER_LOADING_SVG; + const audioObj = new Audio(); + audioObj.preload = 'none'; + voiceTileState.set(tile, { + audio: audioObj, + ringFg, + player, + bufferLayer, + progressSvg, + center, + playing: false, + url, + bufferingUi: false, + }); + player.append(progressSvg, center); + tile.append(textCol, player); + row.append(tile); + const setRingProgress = (t) => { + const a = audioObj; + if (!Number.isFinite(a.duration) || a.duration <= 0) return; + const p = t / a.duration; + ringFg.style.strokeDashoffset = String(RING_C * (1 - p)); + }; + const showPlayIcon = () => setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + const showPauseIcon = () => setVoiceTileCenterIcon(tile, PAF_PP_PAUSE_SVG); + let rafId = null; + const tick = () => { + if (audioObj.paused && !audioObj.ended) { + rafId = null; + return; + } + setRingProgress(audioObj.currentTime); + rafId = requestAnimationFrame(tick); + }; + const startRaf = () => { + if (rafId) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(tick); + }; + const stopRaf = () => { + if (rafId) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + audioObj.addEventListener('loadedmetadata', () => { + const dur = Number.isFinite(audioObj.duration) && audioObj.duration > 0 ? audioObj.duration : 0; + if (dur > 0) widgetInstance.durationCache.set(url, dur); + setRingProgress(0); + }); + audioObj.addEventListener('play', () => { + voiceTileState.get(tile).playing = true; + showPauseIcon(); + tile.setAttribute('aria-pressed', 'true'); + startRaf(); + }); + audioObj.addEventListener('pause', () => { + const atEnd = audioObj.ended || (Number.isFinite(audioObj.duration) && audioObj.currentTime >= audioObj.duration - 0.25); + voiceTileState.get(tile).playing = !atEnd && audioObj.currentTime > 0; + showPlayIcon(); + if (atEnd) { + setRingProgress(0); + ringFg.style.strokeDashoffset = String(RING_C); + } else { + setRingProgress(audioObj.currentTime); + } + tile.setAttribute('aria-pressed', 'false'); + stopRaf(); + }); + audioObj.addEventListener('ended', () => { + voiceTileState.get(tile).playing = false; + showPlayIcon(); + setRingProgress(0); + ringFg.style.strokeDashoffset = String(RING_C); + tile.setAttribute('aria-pressed', 'false'); + try { audioObj.currentTime = 0; } catch (e) { /* noop */ } + stopRaf(); + }); + audioObj.addEventListener('error', () => { + setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + dispatchAudioPlaybackFailed(widgetInstance.widgetWrap); + }); + audioObj.addEventListener('waiting', () => { + if (!audioObj.paused) setVoiceTilePlayerBuffering(tile, true); + }); + audioObj.addEventListener('playing', () => { + if (!audioObj.paused) showPauseIcon(); + }); + return tile; +} + +function attachVoiceInteractivity(tiles, widgetInstance, inpField, voices) { + const wrap = widgetInstance.widgetWrap; + let selectedIdx = 0; + const authoring = (widgetInstance.defaultPromptFromAuthoring ?? '').trim(); + + function setSelectedVisual(idx) { + selectedIdx = idx; + if (idx < 0) { + wrap.removeAttribute('data-selected-voice-index'); + wrap.removeAttribute('data-selected-voice-name'); + wrap.removeAttribute('data-selected-voice-id'); + tiles.forEach((t) => { + t.classList.remove('selected'); + t.removeAttribute('aria-current'); + }); + return; + } + wrap.setAttribute('data-selected-voice-index', String(idx)); + wrap.setAttribute('data-selected-voice-name', voices[idx]?.name ?? ''); + const voiceId = voices[idx]?.voiceId; + if (voiceId) wrap.setAttribute('data-selected-voice-id', voiceId); + else wrap.removeAttribute('data-selected-voice-id'); + tiles.forEach((t, i) => { + t.classList.toggle('selected', i === idx); + if (i === idx) t.setAttribute('aria-current', 'true'); + else t.removeAttribute('aria-current'); + }); + } + + function resetTileIdle(tile) { + const p = voiceTileState.get(tile); + if (!p) return; + try { p.audio.pause(); } catch { /* ignore */ } + try { p.audio.currentTime = 0; } catch { /* ignore */ } + p.playing = false; + p.ringFg.style.strokeDashoffset = String(RING_C); + setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + tile.setAttribute('aria-pressed', 'false'); + } + + function syncPromptIfStuckToDefaults() { + const { value } = inpField; + if (value === authoring || value.trim() === '') { + inpField.value = authoring; + } + } + + function toggleTile(idx) { + const tile = tiles[idx]; + const p = tile && voiceTileState.get(tile); + if (!p) return; + const { audio } = p; + const isPlaying = !audio.paused && !audio.ended; + if (isPlaying) { + audio.pause(); + return; + } + primeVoiceAudioForPlayback(tile); + audio.play().catch(() => { + setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + dispatchAudioPlaybackFailed(wrap); + }); + } + + function onTileActivate(idx) { + if (idx !== selectedIdx) { + syncPromptIfStuckToDefaults(); + setSelectedVisual(idx); + tiles.forEach((t, i) => { if (i !== idx) resetTileIdle(t); }); + const nextTile = tiles[idx]; + primeVoiceAudioForPlayback(nextTile); + voiceTileState.get(nextTile).audio.play().catch(() => { + setVoiceTileCenterIcon(nextTile, PAF_PP_PLAY_SVG); + dispatchAudioPlaybackFailed(wrap); + }); + return; + } + toggleTile(idx); + } + + tiles.forEach((tile, idx) => { + tile.addEventListener('click', (ev) => { + if (ev.target?.closest && ev.target.closest('a[href]')) return; + ev.preventDefault(); + onTileActivate(idx); + }); + tile.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onTileActivate(idx); + } + }); + }); + setSelectedVisual(0); + return () => { tiles.forEach(resetTileIdle); }; +} + +function createPromptAudioShellBase(widgetInstance, el) { + const widgetWrap = createTag('div', { class: 'ex-unity-wrap verb-options' }); + const [widget, unitySprite] = ['ex-unity-widget', 'unity-sprite-container'] + .map((c) => createTag('div', { class: c })); + widgetInstance.widgetWrap = widgetWrap; + widgetInstance.widget = widget; + unitySprite.innerHTML = widgetInstance.spriteCon; + unitySprite.classList.add('unity-slf-sprite'); + widgetWrap.append(unitySprite); + const phStub = createTag('div', { hidden: true, 'aria-hidden': 'true' }); + phStub.innerHTML = '
'; + el.append(phStub); + return { widgetWrap, widget }; +} + +function createPromptAudioInputField(widgetInstance, defaultPrompt, pws) { + const inpField = createTag('textarea', { + id: 'promptInput', + class: 'inp-field', + 'aria-autocomplete': 'list', + 'aria-haspopup': 'listbox', + rows: '4', + }); + inpField.value = defaultPrompt; + let promptEngagedTracked = false; + inpField.addEventListener('pointerdown', (e) => { + if (e.button !== 0 || e.target !== inpField) return; + widgetInstance.hidePromptDropdown(); + if (!promptEngagedTracked && pws?.ENTER_PROMPT) { + promptEngagedTracked = true; + const det = { adobeEventName: pws.ENTER_PROMPT, splunkData: { action: 'enter-prompt' } }; + widgetInstance.widgetWrap?.dispatchEvent(new CustomEvent('firefly-analytics', { detail: det })); + } + }); + inpField.addEventListener('blur', () => { promptEngagedTracked = false; }); + let emptyPromptRestoreTimerId = null; + const clearEmptyPromptRestoreTimer = () => { + if (emptyPromptRestoreTimerId != null) { + clearTimeout(emptyPromptRestoreTimerId); + emptyPromptRestoreTimerId = null; + } + }; + widgetInstance.clearEmptyPromptRestoreTimer = clearEmptyPromptRestoreTimer; + inpField.addEventListener('input', () => { + const trimmed = (inpField.value || '').trim(); + if (trimmed !== '') { + clearEmptyPromptRestoreTimer(); + return; + } + clearEmptyPromptRestoreTimer(); + emptyPromptRestoreTimerId = window.setTimeout(() => { + emptyPromptRestoreTimerId = null; + if (!inpField.isConnected) return; + if ((inpField.value || '').trim() !== '') return; + inpField.value = (widgetInstance.defaultPromptFromAuthoring ?? '').trim(); + }, 10000); + }); + return inpField; +} + +function createPromptAudioActionContainer(widgetInstance, widgetWrap, modelParts) { + const actionContainer = createTag('div', { class: 'action-container' }); + if (modelParts.length > 1) { + const modelBtn = createTag('div', { class: 'models-container', 'aria-label': 'Model options' }); + modelBtn.append(...modelParts); + actionContainer.append(modelBtn); + return actionContainer; + } + widgetWrap.setAttribute('data-selected-model-id', 'adobe-firefly'); + widgetWrap.setAttribute('data-selected-model-version', 'image3'); + const fallbackName = Array.isArray(widgetInstance.models) + ? widgetInstance.models.find((m) => m.id === 'adobe-firefly' && (!m.version || m.version === 'image3'))?.name?.trim() + || widgetInstance.models.find((m) => m.id === 'adobe-firefly')?.name?.trim() + : ''; + if (fallbackName) widgetWrap.setAttribute('data-selected-model-name', fallbackName); + return actionContainer; +} + +function createPromptAudioGenerateButton(widgetInstance, el, pws) { + const generateLi = el.querySelector('.icon-generate')?.closest('li'); + let genBtn = widgetInstance.createActBtn(generateLi, 'gen-btn unity-slf-gen-btn'); + if (!genBtn) { + genBtn = createTag('a', { + href: '#', + class: 'unity-act-btn gen-btn unity-slf-gen-btn', + 'daa-ll': pws?.GENERATE_CTA ?? 'Generate', + 'aria-label': 'Generate', + }); + genBtn.append(createTag('div', { class: 'btn-txt' }, 'Generate')); + widgetInstance.genBtn = genBtn; + return genBtn; + } + if (!genBtn.querySelector('.btn-ico') && generateLi) { + const svgHref = generateLi.querySelector('a[href$=".svg"]')?.href; + if (svgHref) genBtn.prepend(createTag('div', { class: 'btn-ico' }, createTag('img', { src: svgHref, alt: 'Generate' }))); + } + return genBtn; +} + +function composePromptAudioInputLayout(widget, promptLabelText, inpField, actionContainer, genBtn) { + const inpWrap = createTag('div', { class: 'inp-wrap' }); + const promptLabel = createTag( + 'label', + { for: 'promptInput', class: 'unity-slf-copy-label unity-slf-prompt-label' }, + promptLabelText, + ); + const actWrap = createTag('div', { class: 'act-wrap' }); + actWrap.append(genBtn); + inpWrap.append(promptLabel, inpField, actionContainer, actWrap); + const comboboxContainer = createTag('div', { class: 'autocomplete' }); + comboboxContainer.append(inpWrap); + widget.append(comboboxContainer); +} + +function createPromptAudioInputShell(widgetInstance, el, defaultPrompt, analyticsMod) { + const pws = analyticsMod?.PROMPT_WITH_STYLE_EVENTS; + const { widgetWrap, widget } = createPromptAudioShellBase(widgetInstance, el); + widgetInstance.hasModelOptions = !!el.querySelector('[class*="icon-model"]'); + widgetInstance.verbDropdown(); + const modelParts = widgetInstance.modelDropdown(); + const promptLabelText = placeholderRowText(el, 'placeholder-prompt-label'); + const inpField = createPromptAudioInputField(widgetInstance, defaultPrompt, pws); + const actionContainer = createPromptAudioActionContainer(widgetInstance, widgetWrap, modelParts); + const genBtn = createPromptAudioGenerateButton(widgetInstance, el, pws); + composePromptAudioInputLayout(widget, promptLabelText, inpField, actionContainer, genBtn); + widgetWrap.append(widget); + return { widgetWrap, widget, inpField }; +} + +function appendVoiceExploreSubfoot(section, exploreHtml, footerLink) { + const ex = (exploreHtml || '').trim(); + if (ex) { + const wrap = createTag('div', { class: 'unity-paf-voice-subfoot' }); + const p = createTag('p', { class: 'unity-paf-voice-subfoot-line' }); + p.innerHTML = ex; + wrap.append(p); + section.append(wrap); + } else if (footerLink) { + const foot = createTag('p', { class: 'unity-paf-voice-footer' }); + const a = createTag('a', { href: footerLink.href, class: 'unity-paf-voice-footer-link' }); + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.textContent = footerLink.text; + foot.append(a); + section.append(foot); + } +} + +function buildTermsBannerElement(termsHtml) { + const te = (termsHtml || '').trim(); + if (!te) return null; + const outer = createTag('div', { + class: 'unity-paf-terms-banner', + role: 'note', + }); + const p = createTag('p', { class: 'unity-paf-terms-banner-line' }); + p.innerHTML = te; + outer.append(p); + return outer; +} + +function syncVoiceRowPeekClasses(row, shouldPeek) { + if (!row) return; + row.classList.toggle('unity-paf-voice-row-peek', shouldPeek); + row.classList.toggle('unity-paf-voice-row-peek-scrolled', shouldPeek && row.scrollLeft > 0); +} + +function wireVoiceRowPeekTracking(widgetInstance, row, shouldPeek) { + if (widgetInstance.detachVoiceRowPeekTracking) { + try { widgetInstance.detachVoiceRowPeekTracking(); } catch (e) { /* noop */ } + widgetInstance.detachVoiceRowPeekTracking = null; + } + if (!row) { + syncVoiceRowPeekClasses(null, false); + return; + } + const onScroll = () => { + const active = row.classList.contains('unity-paf-voice-row-peek'); + syncVoiceRowPeekClasses(row, active); + }; + row.addEventListener('scroll', onScroll, { passive: true }); + widgetInstance.detachVoiceRowPeekTracking = () => row.removeEventListener('scroll', onScroll); + syncVoiceRowPeekClasses(row, shouldPeek); +} + +function createVoiceStrip(allVoices, visibleVoices, sectionHeading, footerLink, widgetInstance, opts = {}) { + const { exploreHtml = '' } = opts; + if (!allVoices.length) return { section: null, tiles: [] }; + if (!visibleVoices.length) { + const sectionEmpty = createTag('div', { class: 'unity-paf-voice-section' }); + const headingEmpty = createTag( + 'p', + { class: 'unity-slf-copy-label unity-paf-voice-heading' }, + sectionHeading, + ); + const rowEmpty = createTag('div', { class: 'unity-paf-voice-row', role: 'list', 'aria-label': 'Voice samples' }); + sectionEmpty.append(headingEmpty, rowEmpty); + appendVoiceExploreSubfoot(sectionEmpty, exploreHtml, footerLink); + return { section: sectionEmpty, tiles: [] }; + } + const section = createTag('div', { class: 'unity-paf-voice-section' }); + const heading = createTag( + 'p', + { class: 'unity-slf-copy-label unity-paf-voice-heading' }, + sectionHeading, + ); + const row = createTag('div', { class: 'unity-paf-voice-row', role: 'list', 'aria-label': 'Voice samples' }); + syncVoiceRowPeekClasses(row, visibleVoices.length > 4); + const tiles = visibleVoices.map((v, i) => buildVoiceTile(v, i, row, widgetInstance)); + section.append(heading, row); + appendVoiceExploreSubfoot(section, exploreHtml, footerLink); + return { section, tiles }; +} + +function insertPromptBarAudioRoot(el, widgetInstance, widgetWrap, voiceSection, termsBanner) { + const controls = createTag('div', { class: 'unity-slf-controls' }); + controls.append(widgetWrap); + if (voiceSection) controls.append(voiceSection); + const left = createTag('div', { class: 'unity-slf-left' }); + left.append(controls); + const main = createTag('div', { class: 'unity-paf-main' }); + main.append(left); + const skin = el.classList.contains('light') ? 'light' : 'dark'; + const interactiveShell = createTag('div', { class: `interactive-area ${skin}` }); + const root = createTag('div', { class: 'unity-prompt-bar-audio unity-enabled' }); + interactiveShell.append(main); + root.append(interactiveShell); + if (termsBanner) root.append(termsBanner); + const holder = createTag('div', { class: 'unity-slf-config-holder unity-slf-sr-only' }); + holder.setAttribute('aria-hidden', 'true'); + while (el.firstChild) { + holder.append(el.firstChild); + } + el.append(holder); + el.classList.add('unity-prompt-bar-audio-host'); + if (el.parentNode) { + el.parentNode.insertBefore(root, el); + } else { + el.append(root); + } + widgetInstance.promptBarExtendedRoot = root; +} + +async function mountPromptBarAudioUI(widgetInstance, parsed) { + const { + voices, + footerLink, + sectionHeading, + exploreHtml = '', + termsHtml = '', + } = parsed; + const authoring = (widgetInstance.defaultPromptFromAuthoring ?? '').trim(); + const [analyticsMod] = await Promise.all([ + import('../../../scripts/analytics.js'), + widgetInstance.hasModelOptions ? widgetInstance.getModel() : Promise.resolve(), + ]); + promptWithStyleEvents = analyticsMod.PROMPT_WITH_STYLE_EVENTS; + const { el } = widgetInstance; + const { widgetWrap, inpField } = createPromptAudioInputShell(widgetInstance, el, '', analyticsMod); + const selectedModelId = (widgetInstance.selectedModelId + || widgetInstance.widgetWrap?.getAttribute('data-selected-model-id') + || '').trim(); + const visibleVoices = filterVoicesByModelId(voices, widgetInstance.voiceModelIndex, selectedModelId); + inpField.value = authoring; + const { section: voiceSection, tiles } = createVoiceStrip( + voices, + visibleVoices, + sectionHeading, + footerLink, + widgetInstance, + { exploreHtml: exploreHtml || '' }, + ); + const termsBanner = buildTermsBannerElement(termsHtml || ''); + const disconnectFirst = visibleVoices.length + ? attachVoiceInteractivity(tiles, widgetInstance, inpField, visibleVoices) + : () => {}; + widgetInstance.voicePromptInpField = inpField; + widgetInstance.teardownVoiceTiles = disconnectFirst; + widgetInstance.refreshVoiceTilesForModel = function refreshVoiceTilesForModel() { + const all = this.voiceConfigAll; + if (!all || !all.length) return; + const root = this.promptBarExtendedRoot; + const row = root?.querySelector('.unity-paf-voice-row') ?? null; + if (!row) return; + if (this.teardownVoiceTiles) { + try { this.teardownVoiceTiles(); } catch (err) { /* noop */ } + this.teardownVoiceTiles = null; + } + const mid = (this.selectedModelId || this.widgetWrap?.getAttribute('data-selected-model-id') || '').trim(); + const auth = (this.defaultPromptFromAuthoring ?? '').trim(); + row.replaceChildren(); + const visible = filterVoicesByModelId(all, this.voiceModelIndex, mid); + syncVoiceRowPeekClasses(row, visible.length > 4); + if (this.voicePromptInpField) { + const cur = (this.voicePromptInpField.value || '').trim(); + if (visible.length > 0 && (cur === '' || cur === auth)) { + this.voicePromptInpField.value = auth; + } else if (!visible.length) { + this.voicePromptInpField.value = ''; + } + } + if (visible.length === 0) { + this.widgetWrap?.removeAttribute('data-selected-voice-index'); + this.widgetWrap?.removeAttribute('data-selected-voice-name'); + this.widgetWrap?.removeAttribute('data-selected-voice-id'); + return; + } + const newTiles = visible.map((v, i) => buildVoiceTile(v, i, row, this)); + this.teardownVoiceTiles = attachVoiceInteractivity( + newTiles, + this, + this.voicePromptInpField, + visible, + ); + }; + + insertPromptBarAudioRoot(el, widgetInstance, widgetWrap, voiceSection, termsBanner); + const root = widgetInstance.promptBarExtendedRoot; + const initialRow = root?.querySelector('.unity-paf-voice-row') ?? null; + wireVoiceRowPeekTracking(widgetInstance, initialRow, visibleVoices.length > 4); + let removalObserver = null; + let interactivityTornDown = false; + const teardown = () => { + if (interactivityTornDown) return; + interactivityTornDown = true; + widgetInstance.clearEmptyPromptRestoreTimer?.(); + delete widgetInstance.clearEmptyPromptRestoreTimer; + removalObserver?.disconnect(); + removalObserver = null; + if (widgetInstance.teardownVoiceTiles) { + try { widgetInstance.teardownVoiceTiles(); } catch (e) { /* noop */ } + widgetInstance.teardownVoiceTiles = null; + } + if (widgetInstance.detachVoiceRowPeekTracking) { + try { widgetInstance.detachVoiceRowPeekTracking(); } catch (e) { /* noop */ } + widgetInstance.detachVoiceRowPeekTracking = null; + } + delete widgetInstance.refreshVoiceTilesForModel; + if (widgetInstance.disconnectPromptBarAudio === teardown) { + widgetInstance.disconnectPromptBarAudio = null; + } + }; + if (root) { + removalObserver = new MutationObserver(() => { + if (!root.isConnected) teardown(); + }); + removalObserver.observe(document.documentElement, { childList: true, subtree: true }); + } + widgetInstance.disconnectPromptBarAudio = teardown; +} + +export default class PromptBarAudioWidget extends UnityWidget { + constructor(...args) { + super(...args); + this.promptBarExtendedRoot = null; + this.disconnectPromptBarAudio = null; + } + + async initWidget() { + const meta = parsePromptBarAudioAuthoring(this.el); + const { + footerLink, + sectionHeading, + currentPageSourceUrl, + defaultPrompt, + exploreHtml, + termsHtml, + } = meta; + this.defaultPromptFromAuthoring = (defaultPrompt ?? '').trim(); + let voices = []; + if (currentPageSourceUrl) { + try { + voices = await loadVoicesFromCurrentPageJson(currentPageSourceUrl); + } catch (e) { + window.lana?.log(`Message: current page config json load failed, Error: ${e}`, this.lanaOptions); + } + } + this.voices = voices; + this.voiceConfigAll = voices; + this.voiceModelIndex = buildVoiceModelIndex(voices); + const { el } = this; + this.hasModelOptions = !!el.querySelector('[class*="icon-model"]'); + await mountPromptBarAudioUI(this, { + voices, + footerLink, + sectionHeading, + exploreHtml: exploreHtml || '', + termsHtml: termsHtml || '', + }); + const baseMap = this.workflowCfg.targetCfg.actionMap || {}; + return { + ...baseMap, + '.unity-paf-voice-subfoot a': { actionType: 'generate' }, + }; + } +} diff --git a/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js index e5a4144c..84a7df2d 100644 --- a/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js +++ b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js @@ -4,6 +4,7 @@ import { createTag, defineDeviceByScreenSize, + getUnityPromptConfigsBaseUrl, } from '../../../scripts/utils.js'; let promptWithStyleEvents = null; @@ -400,10 +401,7 @@ export class UnityWidget { } async loadModels() { - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const modelFile = `${baseUrl}/unity/configs/prompt/model-picker.json`; const results = await fetch(modelFile); if (!results.ok) { @@ -860,7 +858,7 @@ function insertPromptBarStyleRoot(el, widgetInstance, widgetWrap, styleContainer } else { el.append(root); } - widgetInstance.promptBarStyleRoot = root; + widgetInstance.promptBarExtendedRoot = root; } async function mountPromptBarStyleUI(widgetInstance, parsed) { @@ -878,7 +876,7 @@ async function mountPromptBarStyleUI(widgetInstance, parsed) { const { styleContainer, styleItems, previewArea, styleList } = createStylePreviewSection(styles, previewRows, styleSectionHeadingText); const disconnectInteractivity = attachPromptBarStyleInteractivity(styles, previewRows, inpField, styleItems, previewArea, styleList); insertPromptBarStyleRoot(el, widgetInstance, widgetWrap, styleContainer, previewArea); - const root = widgetInstance.promptBarStyleRoot; + const root = widgetInstance.promptBarExtendedRoot; let removalObserver = null; const teardownPromptBarStyle = () => { removalObserver?.disconnect(); @@ -900,7 +898,7 @@ async function mountPromptBarStyleUI(widgetInstance, parsed) { export default class PromptBarStyleWidget extends UnityWidget { constructor(...args) { super(...args); - this.promptBarStyleRoot = null; + this.promptBarExtendedRoot = null; this.disconnectPromptBarStyle = null; } diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js index f1beb176..87d94e0d 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ -import { createTag, getUnityLibs } from '../../../scripts/utils.js'; +import { createTag, getUnityLibs, getUnityPromptConfigsBaseUrl } from '../../../scripts/utils.js'; function placeholderText(root, iconClass) { const icon = root.querySelector(`.${iconClass}`) || root.querySelector(`[class*="${iconClass}"]`); @@ -227,10 +227,7 @@ export default class PromptBarUploadWidget { } async loadModels() { - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const res = await fetch(`${baseUrl}/unity/configs/prompt/model-picker-video.json`); if (!res.ok) throw new Error('Failed to fetch video models.'); const json = await res.json(); diff --git a/unitylibs/core/widgets/prompt-bar/prompt-bar.js b/unitylibs/core/widgets/prompt-bar/prompt-bar.js index df296cbd..f7a109a9 100644 --- a/unitylibs/core/widgets/prompt-bar/prompt-bar.js +++ b/unitylibs/core/widgets/prompt-bar/prompt-bar.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ -import { createTag, getConfig, unityConfig } from '../../../scripts/utils.js'; +import { createTag, getConfig, getUnityPromptConfigsBaseUrl, unityConfig } from '../../../scripts/utils.js'; export default class UnityWidget { constructor(target, el, workflowCfg, spriteCon) { @@ -516,10 +516,7 @@ export default class UnityWidget { async loadPrompts() { const { locale } = getConfig(); - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const promptFile = locale.prefix && locale.prefix !== '/' ? `${baseUrl}${locale.prefix}/unity/configs/prompt/firefly-prompt.json` : `${baseUrl}/unity/configs/prompt/firefly-prompt.json`; @@ -543,10 +540,7 @@ export default class UnityWidget { } async loadModels() { - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const modelFile = `${baseUrl}/unity/configs/prompt/model-picker.json`; const results = await fetch(modelFile); if (!results.ok) { diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index acb37483..82ed715c 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -35,6 +35,8 @@ export default class ActionBinder { this.unityEl = unityEl; this.workflowCfg = workflowCfg; this.block = block; + this.isPromptBarAudio = !!block?.classList?.contains('unity-prompt-bar-audio'); + this.limits = ActionBinder.resolveLimits(workflowCfg, unityEl, block); this.canvasArea = canvasArea; this.actions = actionMap; this.query = ''; @@ -69,10 +71,27 @@ export default class ActionBinder { this.verb = this.getVerbFromDom(); } + static getLimitsSuffix(unityEl, block) { + const widgetCls = [...(unityEl?.classList || [])].find((c) => c.startsWith('widget-')); + if (widgetCls) return widgetCls.replace(/^widget-/, '').trim(); + const promptBarCls = [...(block?.classList || [])].find( + (c) => c.startsWith('unity-prompt-bar-') && !c.endsWith('-host'), + ); + return promptBarCls ? promptBarCls.replace(/^unity-/, '').trim() : ''; + } + + static resolveLimits(workflowCfg, unityEl, block) { + const targetCfg = workflowCfg?.targetCfg || {}; + const commonLimits = targetCfg.limits || {}; + const widgetSuffix = ActionBinder.getLimitsSuffix(unityEl, block); + const widgetLimits = targetCfg[`limits-${widgetSuffix}`] || {}; + return { ...commonLimits, ...widgetLimits }; + } + getNetworkUtils = async () => { if (this.networkUtils) return this.networkUtils; const { default: NetworkUtils } = await import(`${getUnityLibs()}/utils/NetworkUtils.js`); - return (this.networkUtils = new NetworkUtils()); + return (this.networkUtils = new NetworkUtils()); }; showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') { @@ -234,14 +253,26 @@ export default class ActionBinder { || this.block.querySelector('.models-container .selected-model .model-name')?.textContent?.trim() || ''; + getMaxPromptCharLimit() { + const n = Number(this.limits?.['max-char-limit']); + return Number.isFinite(n) && n > 0 ? n : undefined; + } + validateInput(query) { - if (query.length > 750) { + const maxLen = this.getMaxPromptCharLimit(); + if (maxLen !== undefined && query.length > maxLen) { this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-max-length' }, 'Max prompt characters exceeded'); return { isValid: false, errorCode: 'max-prompt-characters-exceeded' }; } return { isValid: true }; } + getSelectedVoiceIdForConnector() { + if (!this.isPromptBarAudio) return undefined; + const id = this.widgetWrap.getAttribute('data-selected-voice-id')?.trim(); + return id; + } + getSelectedStylePayloadForConnector() { const root = this.block; if (!root?.classList?.contains('unity-prompt-bar-style')) return undefined; @@ -256,6 +287,11 @@ export default class ActionBinder { getSelectedStyleIndexOneBased() { const root = this.block; + if (this.isPromptBarAudio) { + const raw = this.widgetWrap.getAttribute('data-selected-voice-index'); + const idx = raw != null ? parseInt(raw, 10) : NaN; + return Number.isFinite(idx) ? idx + 1 : null; + } if (!root?.classList?.contains('unity-prompt-bar-style')) return null; const items = Array.from(root.querySelectorAll('.unity-slf-style-list .unity-slf-style-item')); const selected = root.querySelector('.unity-slf-style-item.selected'); @@ -304,7 +340,8 @@ export default class ActionBinder { } const selectedVerbType = `text-to-${currentVerb}`; const operationVerb = this.getVerbFromDom(); - const stylePayload = this.getSelectedStylePayloadForConnector(); + const stylePayload = this.isPromptBarAudio ? undefined : this.getSelectedStylePayloadForConnector(); + const voiceId = this.getSelectedVoiceIdForConnector(); const action = (this.id || !!override ? 'prompt-suggestion' : 'generate'); const styleIndexOneBased = this.getSelectedStyleIndexOneBased(); const modelName = this.getSelectedModelDisplayName(); @@ -339,6 +376,7 @@ export default class ActionBinder { ...(modelId ? { modelId } : {}), ...(modelVersion ? { modelVersion } : {}), ...(stylePayload ? { style: stylePayload } : {}), + ...(voiceId ? { voiceId } : {}), locale: getLocale(), action, }, diff --git a/unitylibs/core/workflow/workflow-firefly/sprite.svg b/unitylibs/core/workflow/workflow-firefly/sprite.svg index 868b0567..19231975 100644 --- a/unitylibs/core/workflow/workflow-firefly/sprite.svg +++ b/unitylibs/core/workflow/workflow-firefly/sprite.svg @@ -30,4 +30,17 @@ + + + + + + + + + + + + + diff --git a/unitylibs/core/workflow/workflow-firefly/target-config.json b/unitylibs/core/workflow/workflow-firefly/target-config.json index 1b63130b..0ff7f12d 100644 --- a/unitylibs/core/workflow/workflow-firefly/target-config.json +++ b/unitylibs/core/workflow/workflow-firefly/target-config.json @@ -3,6 +3,13 @@ "type": "text", "handler": "render", "renderWidget": true, + "limits": { + "max-char-limit": 750 + }, + "limits-prompt-bar-audio": { + "max-char-limit": 5000 + }, + "extendedWidgets": ["prompt-bar-style", "prompt-bar-audio"], "verbsWithoutPromptSuggestions": ["vector"], "actionMap": { ".inp-field": { "actionType": "autocomplete" }, diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js index c2328ffa..93af64e0 100644 --- a/unitylibs/core/workflow/workflow.js +++ b/unitylibs/core/workflow/workflow.js @@ -110,10 +110,10 @@ class WfInitiator { this.actionMap = this.targetConfig.actionMap; } const { default: ActionBinder } = await import(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/action-binder.js`); - const isPromptBarStyle = this.widgetName === 'prompt-bar-style'; - const styleRoot = unityWidgetObject?.promptBarStyleRoot; - const actionBinderBlock = isPromptBarStyle ? styleRoot : this.targetBlock; - const canvasAreaForBinder = isPromptBarStyle ? styleRoot : this.interactiveArea; + const isExtendedWidget = (this.targetConfig?.extendedWidgets ?? []).includes(this.widgetName); + const extendedLayoutRoot = isExtendedWidget ? (unityWidgetObject?.promptBarExtendedRoot || null) : null; + const actionBinderBlock = isExtendedWidget ? extendedLayoutRoot : this.targetBlock; + const canvasAreaForBinder = isExtendedWidget ? extendedLayoutRoot : this.interactiveArea; await new ActionBinder( this.el, this.workflowCfg, @@ -178,7 +178,7 @@ class WfInitiator { } createInteractiveArea(block, selector, targetCfg) { - if (this.widgetName === 'prompt-bar-style') return this.el; + if ((targetCfg?.extendedWidgets ?? []).includes(this.widgetName)) return this.el; const iArea = createTag('div', { class: 'interactive-area' }); const asset = block.querySelector(selector); if (asset.nodeName === 'PICTURE') { diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index 31272fcc..8e573d33 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -285,6 +285,14 @@ export function updateQueryParameter(url, paramName = 'format', oldValue = 'webp } } +export function getUnityPromptConfigsBaseUrl() { + const { origin } = window.location; + if (origin.includes('.aem.') || origin.includes('.hlx.')) { + return `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live`; + } + return origin; +} + export const unityConfig = (() => { const { host } = window.location; const commoncfg = { From 6e23d79a3a8d2384900dd3cafb4ce10552541d10 Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Date: Sat, 9 May 2026 00:01:18 -0700 Subject: [PATCH 15/18] MWPW-194184: Fix cancel on 2nd Generate redirecting to Firefly instead of a.com (#768) - Reset assetId in cancelUploadOperation so subsequent Generate calls re-upload the asset with proper AbortController support rather than skipping to continueInApp with a stale assetId and no cancellation hook. Resolves: [MWPW-194184](https://jira.corp.adobe.com/browse/MWPW-194184) **Test URLs:** - Before: https://main--da-cc--adobecom.aem.page/products/firefly/features/ai-video-generator?unitylibs=stage - After: https://main--da-cc--adobecom.aem.page/products/firefly/features/ai-video-generator?unitylibs=MWPW-194184 --- test/core/workflow/workflow.firefly.test.js | 2 +- .../workflow/workflow-prompt-bar-upload/action-binder.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index d86a0ea1..a61c4870 100644 --- a/test/core/workflow/workflow.firefly.test.js +++ b/test/core/workflow/workflow.firefly.test.js @@ -1194,7 +1194,7 @@ describe('Firefly Workflow Tests', () => { let event; beforeEach(() => { - testWidget = new UnityWidget(block, unityElement, workflowCfg, spriteContainer); + testWidget = new UnityWidget(block, unityElement, { ...workflowCfg, targetCfg: { ...workflowCfg.targetCfg } }, spriteContainer); testWidget.widgetWrap = document.createElement('div'); testWidget.widget = document.createElement('div'); testWidget.updateDropdownForVerb = sinon.stub(); diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js index 1caeb049..4551fbf5 100644 --- a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js @@ -108,7 +108,6 @@ export default class ActionBinder { this.limits = workflowCfg.targetCfg?.limits || {}; const productTag = workflowCfg.targetCfg?.[`productTag-${workflowCfg.productName?.toLowerCase()}`] || 'FF'; this.lanaOptions = { sampleRate: 1, tags: `Unity-${productTag}-PBU` }; - } getApiConfig() { @@ -302,6 +301,7 @@ export default class ActionBinder { this.apiConfig.endPoint.assetUpload, { body: JSON.stringify(assetDetails) }, ); + if (signal.aborted) return false; const { id, href, blocksize, uploadUrls } = resJson; this.assetId = id; this.logAnalytics('Asset Created|UnityWidget', { assetId: this.assetId }); @@ -413,7 +413,6 @@ export default class ActionBinder { await this.continueInApp(query, selectedModelId, selectedAspectRatio); } - async continueInApp(query, modelId, aspectRatio) { const { getCgenQueryParams } = await import(`${getUnityLibs()}/utils/cgen-utils.js`); const queryParams = getCgenQueryParams(this.unityEl); @@ -461,6 +460,7 @@ export default class ActionBinder { return response.json(); }, ); + if (this.promiseStack.length > 0) return; this.logAnalytics('Generate Complete|UnityWidget', { assetId: this.assetId }); this.LOADER_LIMIT = 100; if (this.transitionScreen?.splashScreenEl) { @@ -499,6 +499,7 @@ export default class ActionBinder { this.uploadAbortController = null; sendAnalyticsEvent(new CustomEvent('Cancel|UnityWidget')); this.logAnalytics('Cancel|UnityWidget', { assetId: this.assetId }); + this.assetId = null; await this.ensureTransitionScreen(); await this.transitionScreen.showSplashScreen(); const e = new Error('Operation termination requested.'); From 57f359f2e5c52ce7b58bf726a89a7dd8fd09eeba Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Mon, 11 May 2026 20:27:15 +0530 Subject: [PATCH 16/18] MWPW-194159 + Some analytics fixes (#770) Resolves: MWPW-194159 **Test URLs:** Before: https://main--da-cc--adobecom.aem.page/products/firefly/features/ai-video-generator?unitylibs=stage After: https://main--da-cc--adobecom.aem.page/products/firefly/features/ai-video-generator?unitylibs=afixes Co-authored-by: Arushi Gupta --- .../prompt-bar-upload/prompt-bar-upload.css | 1 - .../action-binder.js | 37 ++++++++++++++++--- .../workflow/workflow-upload/action-binder.js | 2 +- unitylibs/scripts/analytics.js | 1 + 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css index 3c4d1752..c48db813 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css @@ -950,7 +950,6 @@ height: 100%; box-sizing: border-box; border-radius: 13.667px; overflow: hidden; -border: 2px solid #2680eb; } .pbu-preview.hidden { diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js index 4551fbf5..13ddfa52 100644 --- a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js @@ -149,7 +149,9 @@ export default class ActionBinder { this.sendAnalyticsToSplunk?.( eventName, this.workflowCfg.productName, - { ...data, operation: this.verb }, + { ...data, + operation: this.verb, + action: 'upload-generate' }, `${unityConfig.apiEndPoint}/log`, true, ); @@ -345,7 +347,11 @@ export default class ActionBinder { } this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, e, this.lanaOptions); this.logAnalytics('Upload server error|UnityWidget', { - errorData: { code: 'error-request', subCode: `uploadAsset ${e.status}`, desc: e.message }, + errorData: { + code: 'error-request', + subCode: `uploadAsset ${e.status}`, + desc: e.message || undefined, + }, assetId: this.assetId, }); return false; @@ -356,12 +362,22 @@ export default class ActionBinder { const maxCharLimit = this.limits?.['max-char-limit'] ?? 1024; if (query.length > maxCharLimit) { this.handleClientError('.icon-error-max-length', 'max-prompt-characters-exceeded', 'Prompt too long'); - this.logAnalytics('generate', { errorData: { code: 'max-prompt-characters-exceeded' } }); return false; } return true; } + async trackUploadFileAttempt(uploadMethod) { + try { + if (!this.analyticsModule) await this.initAnalytics(); + const eventName = this.analyticsModule.PROMPT_BAR_EVENTS.UPLOAD_FILE_ATTEMPT; + sendAnalyticsEvent(new CustomEvent(eventName)); + this.logAnalytics(eventName, { action: uploadMethod }); + } catch (e) { + window.lana?.log(`Message: Upload file attempt analytics failed, Error: ${e}`, this.lanaOptions); + } + } + async ensureTransitionScreen() { if (!this.transitionScreen) { const { default: TransitionScreen } = await import(`${getUnityLibs()}/scripts/transition-screen.js`); @@ -472,8 +488,12 @@ export default class ActionBinder { if (err.message === 'Operation termination requested.') return; await this.transitionScreen?.showSplashScreen(); this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, err, this.lanaOptions); - this.logAnalytics('Generate Error|UnityWidget', { - errorData: { code: 'request-failed', subCode: err.status, desc: err.message }, + this.logAnalytics('Upload server error|UnityWidget', { + errorData: { + code: 'error-request', + subCode: `continueInApp ${err.status}`, + desc: err.message || undefined, + }, assetId: this.assetId, }); window.lana?.log(`Message: Connector call failed, Error: ${err}`, this.lanaOptions); @@ -658,6 +678,7 @@ export default class ActionBinder { e.preventDefault(); dragDepth = 0; setDropzoneHighlight(false); + void this.trackUploadFileAttempt('drop'); sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget')); const files = this.extractFiles(e); await this.executeAction('file-selected', outerMarquee, files); @@ -682,11 +703,15 @@ export default class ActionBinder { el.addEventListener('drop', async (e) => { e.preventDefault(); el.classList.remove('drag-over'); + void this.trackUploadFileAttempt('drop'); sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget')); const files = this.extractFiles(e); await this.executeAction(primaryAction, el, files); }); - el.addEventListener('click', () => { this.block?.querySelector('#file-upload')?.click(); }); + el.addEventListener('click', () => { + void this.trackUploadFileAttempt('drop-zone-click'); + this.block?.querySelector('#file-upload')?.click(); + }); break; case 'INPUT': el.addEventListener('change', async (e) => { diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index ae9c1620..1db1de00 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -329,7 +329,7 @@ export default class ActionBinder { } getVerbFromDom() { - const verbEl = this.unityEl?.querySelector('[class*="icon-verb-"]'); + const verbEl = this.unityEl?.querySelector('[class*="icon-verb-"]') || this.unityEl?.querySelector('[class*="icon-operation-"]'); if (!verbEl) return undefined; const verbClass = Array.from(verbEl.classList).find((cls) => cls.startsWith('icon-verb-')); return verbClass?.slice('icon-verb-'.length); diff --git a/unitylibs/scripts/analytics.js b/unitylibs/scripts/analytics.js index f982ddf3..1ace6fcf 100644 --- a/unitylibs/scripts/analytics.js +++ b/unitylibs/scripts/analytics.js @@ -5,6 +5,7 @@ export const PROMPT_BAR_EVENTS = { MODULE_PICKER: 'Module Picker Select Dropdown|UnityWidget', RATIO_DROPDOWN: 'Ratio Dropdown Select|UnityWidget', MORE: 'More|UnityWidget', + UPLOAD_FILE_ATTEMPT: 'Upload file attempt|UnityWidget', UPLOAD_STARTED: 'Uploading started|UnityWidget', UPLOAD_ERROR: 'Upload error|UnityWidget', generateModel: (modelName) => `Generate ${modelName}|UnityWidget`, From 0eddf8c50063ae5de2ec330bb89b210127e4b06f Mon Sep 17 00:00:00 2001 From: Vipul Gupta Date: Tue, 12 May 2026 12:14:31 +0530 Subject: [PATCH 17/18] Doodlebug Wave 2.2 Audio bug fixes. (#772) Doodlebug Wave 2.2 Audio bug fixes. Resolves: [MWPW-194774](https://jira.corp.adobe.com/browse/MWPW-194774) [MWPW-194791](https://jira.corp.adobe.com/browse/MWPW-194791) [MWPW-194625](https://jira.corp.adobe.com/browse/MWPW-194625) [MWPW-194491](https://jira.corp.adobe.com/browse/MWPW-194491) [MWPW-194785](https://jira.corp.adobe.com/browse/MWPW-194785) [MWPW-194496](https://jira.corp.adobe.com/browse/MWPW-194496) [MWPW-194495](https://jira.corp.adobe.com/browse/MWPW-194495) [MWPW-194494](https://jira.corp.adobe.com/browse/MWPW-194494) [MWPW-194493](https://jira.corp.adobe.com/browse/MWPW-194493) [MWPW-194491](https://jira.corp.adobe.com/browse/MWPW-194491) [MWPW-194489](https://jira.corp.adobe.com/browse/MWPW-194489) [MWPW-194488](https://jira.corp.adobe.com/browse/MWPW-194488) [MWPW-194487](https://jira.corp.adobe.com/browse/MWPW-194487) [MWPW-194486](https://jira.corp.adobe.com/browse/MWPW-194486) **Test URLs:** - Before: https://main--da-cc--adobecom.aem.page/drafts/vipulg/doodlebug/text-to-speech/text-to-speech?unitylibs=stage - After: https://main--da-cc--adobecom.aem.page/drafts/vipulg/doodlebug/text-to-speech/text-to-speech?unitylibs=vg-audio-fixes --------- Co-authored-by: vipulg --- .../prompt-bar-audio/prompt-bar-audio.css | 145 ++++++++++++------ .../prompt-bar-audio/prompt-bar-audio.js | 37 ++++- .../workflow-firefly/action-binder.js | 2 +- .../core/workflow/workflow-firefly/sprite.svg | 6 +- 4 files changed, 141 insertions(+), 49 deletions(-) diff --git a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css index 8bbae74d..e2b23e8f 100644 --- a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css @@ -70,10 +70,15 @@ width: 100%; min-width: 0; overflow-x: auto; - overflow-y: hidden; + overflow-y: visible; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; - padding-bottom: 2px; + padding-inline: 3px; + padding-top: 4px; + padding-bottom: 10px; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: rgb(72 72 72 / 90%) transparent; } .unity-prompt-bar-audio .unity-paf-voice-row.unity-paf-voice-row-peek { @@ -94,8 +99,6 @@ .unity-prompt-bar-audio .unity-paf-voice-tile { --unity-paf-voice-tile-width: 231px; - --paf-voice-tile-border-width: 1.78px; - display: flex; flex-direction: row; align-items: center; @@ -107,18 +110,18 @@ max-width: var(--unity-paf-voice-tile-width); flex: 0 0 var(--unity-paf-voice-tile-width); padding: 14px; - border-radius: 9.51px; - border: var(--paf-voice-tile-border-width) solid transparent; - background: #292929; - backdrop-filter: blur(29.72px); + border-radius: 9.511px; + border: 1.783px solid #323232; + background: #1b1b1b; + backdrop-filter: blur(29.72222328186035px); overflow: hidden; cursor: pointer; text-align: start; - transition: background 0.15s ease, border-color 0.15s ease; + transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; } .unity-prompt-bar-audio .unity-paf-voice-tile:hover { - background: #333; + background: #292929; } .unity-prompt-bar-audio .unity-paf-voice-tile:focus { @@ -126,15 +129,24 @@ } .unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible { - border-color: #274dea; + overflow: visible; + position: relative; + z-index: 1; } .unity-prompt-bar-audio .unity-paf-voice-tile.selected { + background: #292929; border-color: #274dea; } -.unity-prompt-bar-audio .unity-paf-voice-tile.selected:hover { - background: #2f2f2f; +.unity-prompt-bar-audio .unity-paf-voice-tile.selected:focus-visible { + box-shadow: 0 0 0 1px #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible:not(.selected) { + box-shadow: + 0 0 0 1px #274dea, + 0 0 0 2px #fff; } .unity-prompt-bar-audio .unity-paf-voice-tile-text { @@ -176,7 +188,11 @@ flex: 0 0 37.17px; } -@media (hover: hover) and (pointer: fine) { +.unity-prompt-bar-audio .unity-paf-voice-tile .unity-paf-voice-player { + opacity: 1; +} + +@media (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) { .unity-prompt-bar-audio .unity-paf-voice-tile .unity-paf-voice-player { opacity: 0; transition: opacity 0.15s ease; @@ -219,10 +235,11 @@ display: block; width: 18px; height: 18px; + color: #fff; } .unity-prompt-bar-audio .unity-paf-pp-center .unity-paf-pp-svg use { - filter: brightness(0) invert(1); + color: #fff; } .unity-prompt-bar-audio .unity-paf-voice-player-loading { @@ -266,11 +283,11 @@ flex-direction: column; align-items: center; gap: 4px; - margin: 4px 0 0; + margin: 2px 0 0; text-align: center; font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; - font-size: 12px; - line-height: 16px; + font-size: 14px; + line-height: 136%; color: #fff; } @@ -470,12 +487,17 @@ box-shadow: 0 0 10px #0000001c; border-radius: 10px; color: #fff; - margin: 0; animation: none; position: absolute; - top: 0; + top: 100%; left: 0; z-index: 5; + margin: 6px 0 0; +} + +[dir="rtl"] .unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list { + left: unset; + right: 0; } .unity-prompt-bar-audio.unity-enabled .interactive-area .models-container.show-menu .verb-list, @@ -645,30 +667,11 @@ gap: 8px; isolation: isolate; overflow-x: clip; + background: rgba(239, 239, 239, 20%); } -.unity-prompt-bar-audio.unity-enabled > .interactive-area.dark { - background: rgb(24 24 24 / 48%); - backdrop-filter: blur(32px) saturate(165%); - -webkit-backdrop-filter: blur(32px) saturate(165%); - border: 1px solid rgb(255 255 255 / 14%); - box-shadow: - inset 0 1px 0 rgb(255 255 255 / 12%), - 0 16px 48px rgb(0 0 0 / 28%); -} - -.unity-prompt-bar-audio.unity-enabled > .interactive-area.light { - background: rgb(255 255 255 / 52%); - backdrop-filter: blur(32px) saturate(165%); - -webkit-backdrop-filter: blur(32px) saturate(165%); - border: 1px solid rgb(0 0 0 / 8%); - box-shadow: - inset 0 1px 0 rgb(255 255 255 / 85%), - 0 16px 48px rgb(0 0 0 / 10%); -} - -.unity-prompt-bar-audio.unity-enabled .interactive-area.dark .unity-slf-left { - background: rgb(255 255 255 / 5%); +.unity-prompt-bar-audio.unity-enabled > .interactive-area:has(.unity-paf-voice-tile:focus-visible) { + overflow: visible; } .unity-prompt-bar-audio.unity-enabled .interactive-area.light .unity-slf-left { @@ -737,11 +740,12 @@ grid-column: 1 / -1; color: #f8f8f8; padding: 10px 0; + font-size: 16px; } .unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container { grid-column: 1; - margin-top: 32px; + margin-top: 10px; display: flex; align-items: center; gap: 8px; @@ -751,7 +755,6 @@ .unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .act-wrap { grid-column: 2; - margin-top: 24px; display: flex; justify-content: flex-end; align-items: center; @@ -792,11 +795,60 @@ width: fit-content; } +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .alert-holder .alert-toast { + box-sizing: border-box; + max-width: min(239px, 100%); + width: 100%; +} + +@media screen and (min-width: 600px) { + .unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .alert-holder .alert-toast { + max-width: min(363px, 100%); + } +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .alert-holder .alert-toast .alert-content { + min-width: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .alert-holder .alert-toast .alert-content .alert-icon { + flex: 1 1 0%; + justify-content: flex-start; + min-width: 0; + width: auto; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .alert-holder .alert-toast .alert-text { + max-width: 100%; + min-width: 0; + overflow-wrap: break-word; +} + +.hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .copy h1 { + font-weight: 800; + line-height: 98%; +} + @media screen and (max-width: 1199px) { + .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .copy h1 { + font-size: 40px; + letter-spacing: -1.8px; + } + .unity-prompt-bar-audio.unity-enabled { width: var(--grid-container-width); } + .unity-prompt-bar-audio.unity-enabled .interactive-area .models-container .selected-model .model-name { + display: none; + } + + .unity-prompt-bar-audio.unity-enabled .interactive-area .models-container .selected-model { + width: auto; + justify-content: center; + gap: 6px; + } + .unity-prompt-bar-audio .unity-paf-voice-row { gap: 10px; } @@ -823,4 +875,9 @@ .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .main-copy p { max-width: 800px; } + + .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .copy h1 { + font-size: 64px; + letter-spacing: -1.92px; + } } diff --git a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js index 0f09c7ab..b6990dd2 100644 --- a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js @@ -704,6 +704,25 @@ function attachVoiceInteractivity(tiles, widgetInstance, inpField, voices) { toggleTile(idx); } + const visibilityObserver = typeof IntersectionObserver !== 'undefined' + ? new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) return; + const tile = entry.target; + const p = voiceTileState.get(tile); + if (!p?.audio) return; + if (p.audio.paused || p.audio.ended) return; + try { + p.audio.pause(); + } catch { /* ignore */ } + }); + }, { root: null, threshold: 0 }) + : null; + + if (visibilityObserver) { + tiles.forEach((tile) => visibilityObserver.observe(tile)); + } + tiles.forEach((tile, idx) => { tile.addEventListener('click', (ev) => { if (ev.target?.closest && ev.target.closest('a[href]')) return; @@ -714,11 +733,27 @@ function attachVoiceInteractivity(tiles, widgetInstance, inpField, voices) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onTileActivate(idx); + return; + } + if (tiles.length < 2) return; + if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { + e.preventDefault(); + const step = e.key === 'ArrowRight' ? 1 : -1; + const nextIdx = (idx + step + tiles.length) % tiles.length; + const next = tiles[nextIdx]; + next.focus(); + next.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } }); }); setSelectedVisual(0); - return () => { tiles.forEach(resetTileIdle); }; + return () => { + if (visibilityObserver) { + tiles.forEach((tile) => visibilityObserver.unobserve(tile)); + visibilityObserver.disconnect(); + } + tiles.forEach(resetTileIdle); + }; } function createPromptAudioShellBase(widgetInstance, el) { diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index 82ed715c..de03b632 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -53,7 +53,7 @@ export default class ActionBinder { const run = async () => { try { if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast(); - this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-audio-fail' }, ev?.detail?.error, this.lanaOptions, 'client'); + this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, ev?.detail?.error, this.lanaOptions, 'client'); } catch (e) { /* noop */ } }; run(); diff --git a/unitylibs/core/workflow/workflow-firefly/sprite.svg b/unitylibs/core/workflow/workflow-firefly/sprite.svg index 19231975..11393193 100644 --- a/unitylibs/core/workflow/workflow-firefly/sprite.svg +++ b/unitylibs/core/workflow/workflow-firefly/sprite.svg @@ -31,11 +31,11 @@
- - + + - + From f7dc912b8742d94653e60e1481a35908f2f05bda Mon Sep 17 00:00:00 2001 From: vipulg Date: Tue, 12 May 2026 16:29:34 +0530 Subject: [PATCH 18/18] [DOTCOM-185216] Accessibility fixes for Doodlebug Video wave --- .../prompt-bar-upload/prompt-bar-upload.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js index 87d94e0d..021e7d89 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -79,9 +79,16 @@ function closeDropdown(container, triggerBtn, list) { triggerBtn.setAttribute('aria-expanded', 'false'); } +function setComboboxTriggerAriaLabel(triggerBtn, nameContainer) { + const v = (nameContainer.textContent || '').trim(); + const prefix = triggerBtn.dataset.comboboxLabel || ''; + triggerBtn.setAttribute('aria-label', v ? `${prefix}, ${v}` : prefix); +} + function buildDropdownShell({ label, menuId, extraClass = '', imgEl = null, ariaLabelledBy = null }) { const container = createTag('div', { class: `models-container${extraClass ? ` ${extraClass}` : ''}`, + role: 'group', 'aria-label': label, }); @@ -96,6 +103,7 @@ function buildDropdownShell({ label, menuId, extraClass = '', imgEl = null, aria 'aria-haspopup': 'listbox', role: 'combobox', }); + triggerBtn.dataset.comboboxLabel = label; if (imgEl) triggerBtn.append(imgEl, nameContainer, menuIcon); else triggerBtn.append(nameContainer, menuIcon); @@ -105,9 +113,7 @@ function buildDropdownShell({ label, menuId, extraClass = '', imgEl = null, aria list.setAttribute('style', 'display: none;'); container.append(triggerBtn, list); - return { - container, triggerBtn, nameContainer, menuIcon, list, - }; + return { container, triggerBtn, nameContainer, menuIcon, list }; } function attachDropdownBehavior(container, triggerBtn, list) { @@ -289,6 +295,7 @@ export default class PromptBarUploadWidget { ariaLabelledBy: 'listbox-label', }); nameContainer.textContent = (defaultModel?.name || '').trim(); + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); this.models.forEach((model, idx) => { const selectedIcon = createTag('span', { class: 'selected-icon' }, svgIcon('#unity-checkmark-icon')); @@ -322,6 +329,7 @@ export default class PromptBarUploadWidget { const modelVersion = link.getAttribute('data-model-version') || ''; this.selectedModelId = modelId; nameContainer.textContent = modelName; + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); const triggerIcon = triggerBtn.querySelector(':scope > img'); if (modelIcon) { if (triggerIcon) { @@ -391,6 +399,7 @@ export default class PromptBarUploadWidget { imgEl: triggerAspectIcon, }); nameContainer.textContent = ratios[0]; + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); ratios.forEach((ratio, idx) => { const selectedIcon = createTag('span', { class: 'selected-icon' }, svgIcon('#unity-checkmark-icon')); @@ -414,6 +423,7 @@ export default class PromptBarUploadWidget { e.stopPropagation(); const ratio = link.getAttribute('data-ratio') || ''; nameContainer.textContent = ratio; + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); setAspectRatioTriggerIconSvg(triggerAspectIcon, ratio); this.setSelectedAspectRatio(modelId, ratio); syncDropdownSelection(list, link); @@ -495,7 +505,11 @@ export default class PromptBarUploadWidget { }); const dropContent = createTag('div', { class: 'pbu-drop-content' }); - dropContent.append(createTag('img', { loading: 'lazy', src: `${getUnityLibs()}/img/icons/upload.svg` })); + dropContent.append(createTag('img', { + loading: 'lazy', + src: `${getUnityLibs()}/img/icons/upload.svg`, + alt: 'Upload image', + })); const dropZone = createTag('div', { class: 'drop-zone', role: 'button',