diff --git a/test/core/workflow/workflow-acrobat/action-binder.test.js b/test/core/workflow/workflow-acrobat/action-binder.test.js index 1de88fae9..6dc4f1526 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/test/core/workflow/workflow-upload/action-binder.test.js b/test/core/workflow/workflow-upload/action-binder.test.js index 6af3f09f2..28b5b6c18 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/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index 3f0ddd662..a61c48701 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'); @@ -1188,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(); @@ -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/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 3a397cacd..25f1378f2 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/blocks/unity/unity.js b/unitylibs/blocks/unity/unity.js index b52e425f6..4c9a896bf 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/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 000000000..e2b23e8f6 --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css @@ -0,0 +1,883 @@ +.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: visible; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; + 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 { + 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; + 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.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, box-shadow 0.15s ease; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:hover { + background: #292929; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus { + outline: none; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible { + 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: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 { + 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; +} + +.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; + } + + .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; + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-pp-center .unity-paf-pp-svg use { + color: #fff; +} + +.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: 2px 0 0; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 14px; + line-height: 136%; + 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; + animation: none; + position: absolute; + 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, +.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; + background: rgba(239, 239, 239, 20%); +} + +.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 { + 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; + 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: 10px; + 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; + 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; +} + +.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; + } + + .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; + } + + .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 new file mode 100644 index 000000000..b6990dd29 --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js @@ -0,0 +1,1154 @@ +/* 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); + } + + 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; + ev.preventDefault(); + onTileActivate(idx); + }); + tile.addEventListener('keydown', (e) => { + 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 () => { + if (visibilityObserver) { + tiles.forEach((tile) => visibilityObserver.unobserve(tile)); + visibilityObserver.disconnect(); + } + 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.css b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.css index c743bedaa..b7dd5bcb7 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-style/prompt-bar-style.js b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js index e5a4144cf..84a7df2d2 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.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css new file mode 100644 index 000000000..c48db813a --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css @@ -0,0 +1,1221 @@ +.upload-marquee.unity-enabled .interactive-area { + display: inherit; + background: none; +} + +.unity-prompt-bar-upload.unity-enabled { +width: 100%; +max-width: 100%; +margin-top: 24px; +box-sizing: border-box; +} + +.unity-prompt-bar-upload.unity-enabled .interactive-area { +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; +} + +@media screen and (min-width: 1200px) { + .unity-prompt-bar-upload.unity-enabled { + max-width: 1000px; + } + + .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 { +font-size: 14px; +width: fit-content; +max-width: 100%; +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; +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 1 0%; +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; +} + +.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; +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 { +flex: 1; +min-height: 80px; +resize: none; +width: 100%; +max-width: 100%; +align-self: stretch; +box-sizing: border-box; +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 { +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: nowrap; +align-items: center; +gap: 8px; +justify-content: flex-start; +flex: 1 1 auto; +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: 0 0 auto; +flex-shrink: 0; +min-width: 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 .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; +font-size: 14px; +font-weight: 400; +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, +.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; +padding-top: 10px; +} + +.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 .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; +} + +.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; +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, +.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: 6px 10px; +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.light .selected-model { +display: inline-flex; +align-items: center; +gap: 8px; +justify-content: flex-start; +padding: 6px 10px; +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; +} + +: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: 14px; +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: 100%; +bottom: auto; +left: 0; +transform: none; +animation: none; +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, +.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, +.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container.show-menu .verb-list { +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; +} + +.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: 100%; +height: 100%; +box-sizing: border-box; +border-radius: 13.667px; +overflow: hidden; +} + +.pbu-preview.hidden { +display: none; +} + +.pbu-preview-img { +display: block; +width: stretch; +height: stretch; +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: 100%; +height: 100%; +min-height: 0; +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; +font-size: 14px; +font-weight: 400; +} + +.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; +white-space: nowrap; +flex-shrink: 0; +} + +.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 { + max-width: 130px; + 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; + } + + .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 new file mode 100644 index 000000000..021e7d89b --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -0,0 +1,652 @@ +/* eslint-disable no-await-in-loop */ + +import { createTag, getUnityLibs, getUnityPromptConfigsBaseUrl } 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 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-square-icon'; +} + +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) { + 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 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, + }); + + 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', + }); + triggerBtn.dataset.comboboxLabel = label; + 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 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(); + 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(); + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); + + 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; + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); + 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 triggerAspectIcon = createAspectRatioIconSpan(ratios[0], true); + const { container, triggerBtn, nameContainer, list } = buildDropdownShell({ + label: 'Aspect ratio', + menuId: 'pbu-aspect-menu', + extraClass: 'pbu-aspect-models', + imgEl: triggerAspectIcon, + }); + nameContainer.textContent = ratios[0]; + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); + + 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, 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); + }); + + 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; + setComboboxTriggerAriaLabel(triggerBtn, nameContainer); + setAspectRatioTriggerIconSvg(triggerAspectIcon, 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`, + alt: 'Upload image', + })); + 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 textarea = createTag('textarea', { + id: 'pbuPromptInput', + class: 'inp-field', + rows: '1', + '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/widgets/prompt-bar/prompt-bar.js b/unitylibs/core/widgets/prompt-bar/prompt-bar.js index df296cbd6..f7a109a92 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-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 7af73e300..e4343970b 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 = { @@ -161,7 +162,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 +183,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 +228,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 +248,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 +257,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 +477,7 @@ export default class ActionBinder { redirectUrl = url.href; } } - this.redirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; }) .catch(async (e) => { await this.showTransitionScreen(); @@ -486,7 +506,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 +576,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/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index e4f1d1c93..2a89e528c 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-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index acb374833..de03b632f 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 = ''; @@ -51,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(); @@ -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 868b0567a..113931930 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 1b63130b2..0ff7f12df 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-prompt-bar-upload/action-binder.js b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js new file mode 100644 index 000000000..13ddfa52f --- /dev/null +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js @@ -0,0 +1,759 @@ +/* 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, + action: 'upload-generate' }, + `${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) }, + ); + if (signal.aborted) return false; + 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 || undefined, + }, + assetId: this.assetId, + }); + return false; + } + } + + validateInput(query) { + 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'); + 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`); + this.transitionScreen = new TransitionScreen(null, this.initActionListeners, this.LOADER_LIMIT, this.workflowCfg, this.desktop); + } + if (!this.transitionScreen.splashScreenEl) { + await this.transitionScreen.loadSplashFragment(); + } + } + + async handleGenerate(isGenerateCta = 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 = isGenerateCta ? 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); + } + + 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') || ''; + 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: false, + }, + }; + 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(); + }, + ); + if (this.promiseStack.length > 0) return; + 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('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); + } + } + + 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 }); + this.assetId = null; + 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); + void this.trackUploadFileAttempt('drop'); + 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'); + 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', () => { + void this.trackUploadFileAttempt('drop-zone-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 000000000..d1276aa7d --- /dev/null +++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg @@ -0,0 +1,60 @@ + 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 000000000..cc5190022 --- /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 d74726f91..1db1de008 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`, @@ -319,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); @@ -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 0648b0df9..9f6326f80 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 1a726663d..93af64e0c 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( @@ -115,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, @@ -183,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') { @@ -255,6 +250,7 @@ class WfInitiator { 'heic-to-pdf', 'quiz-maker', 'flashcard-maker', + 'mindmap-maker', ]), }, 'workflow-ai': { @@ -267,6 +263,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 000000000..bdec86ab3 --- /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 628fd0d2c..1ace6fcfd 100644 --- a/unitylibs/scripts/analytics.js +++ b/unitylibs/scripts/analytics.js @@ -1,10 +1,19 @@ -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_FILE_ATTEMPT: 'Upload file attempt|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 +37,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 +54,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 800b16428..2f9c83d61 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/scripts/utils.js b/unitylibs/scripts/utils.js index b8fc7c577..8e573d33c 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -5,9 +5,11 @@ 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 (!/^[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'; return branch.includes('--') ? `https://${branch}.${env}.live/libs` : `https://${branch}--milo--adobecom.${env}.live/libs`; })(); @@ -29,7 +31,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, @@ -283,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 = { @@ -297,12 +307,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 +347,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/FileUtils.js b/unitylibs/utils/FileUtils.js index f89762154..8917cd8c0 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); diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index 013755c2c..f102ba724 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'); }