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');
}