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/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-upload/prompt-bar-upload.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css
new file mode 100644
index 000000000..b29feb7c7
--- /dev/null
+++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css
@@ -0,0 +1,918 @@
+.upload-marquee.unity-enabled .interactive-area {
+ display: inherit;
+ background: none;
+}
+
+.unity-prompt-bar-upload.unity-enabled {
+width: 100%;
+max-width: 1000px;
+margin-top: 24px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area {
+width: 727px;
+background: #222 !important;
+border: none !important;
+box-shadow: none !important;
+border-radius: 20px !important;
+padding: 8px !important;
+box-sizing: border-box !important;
+max-width: 727px;
+}
+
+@media screen and (min-width: 1200px) {
+.unity-prompt-bar-upload.unity-enabled .interactive-area {
+ padding: 14px !important;
+}
+}
+
+.ex-unity-wrap.pbu-widget .pbu-legal-foot {
+font-size: 14px;
+width: fit-content;
+max-width: 727px;
+box-sizing: border-box;
+font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif;
+font-size: 13px;
+font-weight: 400;
+line-height: 1.45;
+color: #f8f8f8;
+text-align: start;
+margin-top: 12px;
+}
+
+.ex-unity-wrap.pbu-widget .pbu-legal-foot a {
+color: #1473E6;
+text-decoration: none;
+}
+
+.ex-unity-wrap.pbu-widget .pbu-legal-foot a:hover {
+text-decoration: underline;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-main {
+position: relative;
+padding: 16px;
+border-radius: 10px;
+background: #1B1B1B;
+display: flex;
+flex-direction: row;
+align-items: flex-start;
+gap: 16px;
+width: 100%;
+box-sizing: border-box;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-left-section {
+display: flex;
+flex-direction: column;
+align-items: flex-start;
+gap: 17px;
+flex-shrink: 0;
+border-right: 1px solid rgb(255 255 255 / 12%);
+padding-right: 16px;
+box-sizing: border-box;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-right-section {
+display: flex;
+flex-direction: column;
+align-items: stretch;
+gap: 0;
+flex: 1;
+min-width: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-right-section .pbu-prompt-bar-container {
+display: flex;
+flex-direction: column;
+gap: 0;
+width: 100%;
+min-width: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-main .alert-holder {
+position: absolute;
+inset: 0;
+width: 100%;
+height: 100%;
+display: none;
+align-items: center;
+justify-content: center;
+padding: 12px;
+box-sizing: border-box;
+z-index: 350;
+border-radius: 10px;
+background: rgb(0 0 0 / 60%);
+pointer-events: none;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-main .alert-holder.show {
+display: flex;
+pointer-events: auto;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-main .alert-holder .alert-toast {
+position: relative;
+left: auto;
+right: auto;
+top: auto;
+bottom: auto;
+margin: 0 auto;
+width: min(339px, 100%);
+max-width: 100%;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-right-section .unity-slf-prompt-label {
+margin-bottom: 0px;
+padding: 0;
+display: block;
+flex-shrink: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-right-section .inp-field {
+flex: 1;
+min-height: 80px;
+resize: none;
+width: 100%;
+box-sizing: border-box;
+padding-bottom: 10px;
+margin: 0 0 12px 0;
+border: none;
+background: transparent;
+outline: none;
+font-size: var(--type-body-s-size, 15px);
+line-height: 1.45;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer {
+display: flex;
+flex-direction: row;
+align-items: center;
+gap: 8px;
+width: 100%;
+box-sizing: border-box;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container {
+display: flex !important;
+flex-wrap: wrap;
+align-items: center;
+gap: 8px;
+justify-content: flex-start;
+flex: 1;
+margin-top: 0 !important;
+min-width: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap {
+display: flex !important;
+align-items: center;
+gap: 10px;
+justify-content: flex-end;
+margin-top: 0 !important;
+flex-shrink: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container:empty ~ .act-wrap {
+flex: 1;
+justify-content: flex-end;
+}
+
+.unity-prompt-bar-upload.unity-enabled .unity-slf-copy-label {
+color: #d1d1d1;
+font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif;
+font-size: 14px;
+font-weight: 400;
+line-height: 18px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .unity-slf-prompt-label {
+display: block;
+margin-bottom: 8px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .inp-field,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .inp-field::placeholder {
+color: #f8f8f8 !important;
+margin-bottom: 18px;
+font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif;
+font-size: 16px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .inp-field,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .inp-field::placeholder {
+color: #292929;
+}
+
+@keyframes pbu-model-fade-in {
+0% { opacity: 0; }
+100% { opacity: 1; }
+}
+
+@keyframes pbu-model-move-down {
+0% {
+ transform: translateY(33px);
+ opacity: 0;
+ display: none;
+}
+100% {
+ transform: translateY(40px);
+ opacity: 1;
+}
+}
+
+@keyframes pbu-model-move-up {
+0% {
+ transform: translateY(40px);
+ opacity: 1;
+}
+100% {
+ transform: translateY(33px);
+ opacity: 0;
+ display: none;
+}
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model img,
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link img {
+width: 22px;
+height: 22px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon,
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon {
+font-size: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon,
+.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .menu-icon svg,
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon,
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link .selected-icon svg {
+width: 12px;
+height: 12px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .models-container {
+display: flex;
+flex: 0 1 auto;
+max-width: none;
+width: auto;
+position: relative;
+align-items: center;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .pbu-controls-footer .models-container {
+max-width: none;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .verb-list .verb-link,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .verb-list .verb-link .model-name {
+color: #292929;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container:not(.pbu-aspect-models) .verb-list {
+min-width: 270px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container .menu-icon {
+position: relative;
+top: 1px;
+display: flex;
+align-items: center;
+justify-content: center;
+flex-shrink: 0;
+transition: transform 0.15s ease-in;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .menu-icon {
+transform: rotate(-180deg);
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model {
+display: inline-flex;
+align-items: center;
+gap: 8px;
+justify-content: flex-start;
+padding: 8px 12px;
+min-height: 32px;
+width: auto;
+min-width: 27px;
+max-width: 200px;
+box-sizing: border-box;
+cursor: pointer;
+border: none;
+font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif;
+font-size: 14px;
+font-weight: 400;
+line-height: 1.2;
+text-transform: none;
+white-space: nowrap;
+background: #353535;
+color: #fff;
+border-radius: 10px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model:hover {
+background: #434343;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model:focus-visible {
+outline: 2px solid #2680eb;
+outline-offset: 2px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model .model-name {
+color: #fff;
+overflow: hidden;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model .menu-icon svg,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .models-container .menu-icon svg {
+filter: brightness(0) invert(1);
+opacity: 0.95;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model img {
+width: 20px;
+height: 20px;
+border-radius: 6px;
+flex-shrink: 0;
+object-fit: cover;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .pbu-aspect-models .selected-model:not(:has(img))::before {
+content: '';
+display: block;
+width: 22px;
+height: 12px;
+box-sizing: border-box;
+border: 1.5px solid rgb(255 255 255 / 92%);
+border-radius: 2px;
+flex-shrink: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model {
+display: inline-flex;
+align-items: center;
+gap: 8px;
+justify-content: flex-start;
+padding: 8px 12px;
+min-height: 40px;
+width: auto;
+border: none;
+border-radius: 10px;
+background: rgb(0 0 0 / 8%);
+color: #292929;
+font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif;
+font-size: 14px;
+font-weight: 400;
+cursor: pointer;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model .model-name {
+color: #292929;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model .menu-icon svg,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .models-container .menu-icon svg {
+filter: brightness(0);
+opacity: 0.85;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.light .pbu-aspect-models .selected-model:not(:has(img))::before {
+content: '';
+display: block;
+width: 22px;
+height: 12px;
+box-sizing: border-box;
+border: 1.5px solid rgb(0 0 0 / 55%);
+border-radius: 2px;
+flex-shrink: 0;
+}
+
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model,
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model {
+border-radius: 12px;
+min-height: 36px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list {
+padding: 18px;
+list-style: none;
+box-shadow: 0 0 10px #0000001c;
+border-radius: 10px;
+background: rgb(255 255 255 / 100%);
+color: #292929;
+margin: 0;
+min-width: 110px;
+animation: pbu-model-move-up 0.2s ease forwards;
+position: absolute;
+top: 0;
+left: 0;
+z-index: 1;
+}
+
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list {
+border-radius: 14px;
+}
+
+[lang="ja-JP"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list,
+[lang="ko-KR"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list {
+margin-top: 6px;
+}
+
+[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list {
+left: unset;
+right: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .verb-list {
+display: block;
+animation: pbu-model-move-down 0.4s cubic-bezier(0.5, 1.8, 0.3, 0.8) forwards;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link {
+display: flex;
+align-items: center;
+gap: 10px;
+padding: 10px;
+padding-inline-start: 25px;
+text-transform: capitalize;
+text-decoration: none;
+text-align: start;
+position: relative;
+opacity: 0;
+animation: pbu-model-fade-in 0.5s ease forwards;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .models-container .verb-list,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verbs-container .verb-list {
+background: #000;
+color: #f8f8f8;
+box-shadow: 0 8px 32px rgb(0 0 0 / 55%);
+border: 1px solid rgb(255 255 255 / 10%);
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .model-name {
+color: #f8f8f8;
+text-transform: none;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link:hover,
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link:focus-visible {
+background: transparent;
+border-radius: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-item.selected .verb-link {
+background: transparent;
+border-radius: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .selected-icon {
+flex-shrink: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area.dark .verb-list .verb-link .selected-icon svg {
+width: 12px;
+height: 12px;
+display: block;
+}
+
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-link {
+font-size: 14px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .model-link {
+font-size: 14px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-item .selected-icon {
+display: none;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-item.selected .selected-icon {
+display: block;
+position: absolute;
+top: 50%;
+left: 3px;
+transform: translateY(-50%);
+}
+
+[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list .verb-item.selected .selected-icon {
+right: 3px;
+left: unset;
+}
+
+@media (max-width: 1024px) {
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .verb-list {
+ animation: pbu-model-move-down 0.4s cubic-bezier(0.5, 1.8, 0.3, 0.8) forwards;
+}
+}
+
+@media screen and (max-width: 599px) {
+.unity-prompt-bar-upload.unity-enabled .interactive-area .models-container {
+ width: auto;
+}
+
+[dir="rtl"] .unity-prompt-bar-upload.unity-enabled .interactive-area .models-container.show-menu .verb-list {
+ left: unset;
+}
+
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .verb-list {
+ top: unset;
+ left: unset;
+}
+
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .models-container .verb-list {
+ left: 0;
+ width: 100%;
+ box-sizing: border-box;
+ margin-top: 30px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .interactive-area .selected-model .model-name {
+ display: none;
+}
+
+:root:has(meta[name="theme"][content="max25"], .theme-two) .unity-prompt-bar-upload.unity-enabled .interactive-area .models-container {
+ position: static;
+}
+}
+
+.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn {
+background: transparent;
+border: none;
+padding: 7px 12px;
+gap: 6px;
+color: #f8f8f8;
+}
+
+.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-ico {
+display: flex;
+align-items: center;
+flex-shrink: 0;
+}
+
+.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-ico svg {
+width: 20px;
+height: 20px;
+}
+
+.unity-prompt-bar-upload.unity-enabled .act-wrap .pbu-more-btn .btn-txt {
+color: #f8f8f8;
+font-size: 15px;
+font-weight: 400;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container .verb-list,
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container .verb-list {
+top: auto;
+bottom: 100%;
+transform: none;
+animation: none;
+margin-bottom: 4px;
+z-index: 10;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container,
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container {
+position: relative;
+z-index: 1;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .models-container.show-menu,
+.unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .verbs-container.show-menu {
+z-index: 300;
+}
+
+.pbu-drop-zone-wrap {
+position: relative;
+width: 123px;
+height: 123px;
+min-height: 123px;
+flex-shrink: 0;
+box-sizing: border-box;
+}
+
+.pbu-drop-zone-wrap .drop-zone {
+margin-top: 0px;
+width: 123px;
+height: 123px;
+border-radius: 13.667px;
+border: 2px dashed rgba(198, 198, 198, 0.50);
+padding: 0;
+background: none;
+display: flex;
+flex-direction: column;
+align-items: center;
+justify-content: center;
+gap: 8px;
+box-sizing: border-box;
+}
+
+.pbu-drop-zone-wrap .drop-zone:hover,
+.pbu-drop-zone-wrap .drop-zone.drag-over {
+border-color: rgb(255 255 255 / 55%);
+background: rgb(255 255 255 / 4%);
+}
+
+.pbu-drop-zone-wrap .drop-zone.drag-over {
+border-color: #4069FD;
+border: 2px solid;
+background: rgb(64 105 253 / 14%);
+}
+
+.pbu-drop-zone-wrap .drop-zone.hidden {
+display: none;
+}
+
+.pbu-drop-content {
+display: flex;
+flex-direction: column;
+align-items: center;
+gap: 6px;
+pointer-events: none;
+}
+
+.pbu-upload-svg {
+width: 28px;
+height: 28px;
+color: rgb(255 255 255 / 65%);
+}
+
+.pbu-upload-text {
+font-size: 13px;
+line-height: 1.35;
+color: rgb(255 255 255 / 70%);
+}
+
+.pbu-legal-text {
+font-size: 10px;
+color: rgb(255 255 255 / 45%);
+margin: 0;
+}
+
+.pbu-preview {
+position: absolute;
+top: 0;
+left: 0;
+width: 123px;
+height: 123px;
+box-sizing: border-box;
+border-radius: 13.667px;
+overflow: hidden;
+border: 2px solid #2680eb;
+background: rgb(0 0 0 / 20%);
+}
+
+.pbu-preview.hidden {
+display: none;
+}
+
+.pbu-preview-img {
+display: block;
+width: 100%;
+height: 100%;
+object-fit: cover;
+border-radius: 13.667px;
+border: 2px solid #4069FD;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-delete-btn {
+border-radius: 16px;
+background: rgb(255 255 255 / 94%);
+box-shadow: 0 2px 8px 0 rgb(0 0 0 / 16%);
+display: flex;
+justify-content: center;
+align-items: center;
+position: absolute;
+top: 50%;
+left: 50%;
+right: auto;
+transform: translate(-50%, -50%);
+width: 32px;
+height: 32px;
+padding: 0;
+border: none;
+cursor: pointer;
+opacity: 0;
+transition: opacity 0.2s;
+z-index: 2;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-delete-btn svg {
+display: block;
+width: 18px;
+height: 18px;
+}
+
+.pbu-drop-zone-wrap .pbu-select-spinner {
+position: absolute;
+top: 0;
+left: 0;
+width: 123px;
+height: 123px;
+box-sizing: border-box;
+border-radius: 13.667px;
+display: flex;
+align-items: center;
+justify-content: center;
+background: rgb(0 0 0 / 35%);
+z-index: 3;
+}
+
+.pbu-drop-zone-wrap.pbu-select-processing .drop-zone {
+background: #000;
+border: 2px solid #4069FD;
+border-style: solid;
+}
+
+.pbu-drop-zone-wrap.pbu-select-processing .pbu-drop-content {
+visibility: hidden;
+pointer-events: none;
+}
+
+.pbu-drop-zone-wrap.pbu-select-processing .pbu-select-spinner {
+background: transparent;
+}
+
+.pbu-drop-zone-wrap .pbu-select-spinner.hidden {
+display: none;
+}
+
+.pbu-select-spinner-ring {
+width: 32px;
+height: 32px;
+border: 3px solid rgb(255 255 255 / 35%);
+border-top-color: #fff;
+border-radius: 50%;
+animation: pbu-spin 0.7s linear infinite;
+}
+
+.unity-prompt-bar-upload.unity-enabled .pbu-preview:hover .pbu-delete-btn,
+.unity-prompt-bar-upload.unity-enabled .pbu-delete-btn:focus-visible {
+opacity: 1;
+}
+
+.pbu-spinner {
+position: absolute;
+inset: 0;
+display: flex;
+align-items: center;
+justify-content: center;
+background: rgb(0 0 0 / 35%);
+}
+
+.pbu-spinner.hidden {
+display: none;
+}
+
+.pbu-spinner::after {
+content: '';
+width: 32px;
+height: 32px;
+border: 3px solid rgb(255 255 255 / 35%);
+border-top-color: #fff;
+border-radius: 50%;
+animation: pbu-spin 0.7s linear infinite;
+}
+
+@keyframes pbu-spin {
+to { transform: rotate(360deg); }
+}
+
+.action-container > a.unity-act-btn.pbu-more-btn.more-btn {
+display: flex;
+gap: 6px;
+text-decoration: none;
+}
+
+.action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico {
+height: 20px;
+width: 20px;;
+padding: 6px 0px 6px 10px;
+align-self: center;
+}
+
+.action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-ico svg {
+width: 20px;
+height: 20px;
+}
+.action-container > a.unity-act-btn.pbu-more-btn.more-btn .btn-txt {
+color: #C6C6C6;
+}
+
+.pbu-main .pbu-controls-footer .act-wrap a.gen-btn {
+border-radius: 25px;
+background: linear-gradient(90deg, #D73220 0%, #D92361 33%, #7155FA 100%);
+border: none;
+padding: 10px 20px 10px 18px;
+gap: 8px;
+text-decoration: none;
+display: flex;
+align-items: center;
+}
+
+.pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-ico {
+height: fit-content;
+display: flex;
+}
+
+.pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-ico img {
+width: 22px;
+height: 22px;
+}
+
+.pbu-main .pbu-controls-footer .act-wrap a.gen-btn .btn-txt{
+color: var(--color-white);
+font-size: 16px;
+font-weight: 700;
+line-height: normal;
+}
+
+@media screen and (max-width: 599px) {
+ .unity-prompt-bar-upload.unity-enabled .interactive-area {
+ width: 100%;
+ max-width: 100%;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-main {
+ flex-direction: column;
+ gap: 0;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-left-section {
+ width: 100%;
+ max-width: 100%;
+ border-right: none;
+ padding-right: 0;
+ padding-bottom: 16px;
+ margin-bottom: 0;
+ border-bottom: 1px solid rgb(255 255 255 / 12%);
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-drop-zone-wrap {
+ width: 100%;
+ height: 160px;
+ min-height: 160px;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-drop-zone-wrap .drop-zone {
+ width: 100%;
+ height: 160px;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-preview {
+ width: 100%;
+ height: 160px;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-drop-zone-wrap .pbu-select-spinner {
+ width: 100%;
+ height: 160px;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-right-section {
+ width: 100%;
+ padding-top: 12px;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer {
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .action-container {
+ flex: 0 0 auto;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap {
+ flex: 1;
+ justify-content: flex-end;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-controls-footer .act-wrap .gen-btn {
+ flex: 1;
+ justify-content: center;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .interactive-area .pbu-controls-footer .pbu-aspect-models {
+ display: none;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .pbu-more-btn .btn-txt {
+ display: none;
+ }
+
+ .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model,
+ .unity-prompt-bar-upload.unity-enabled .interactive-area.dark .selected-model:hover,
+ .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model,
+ .unity-prompt-bar-upload.unity-enabled .interactive-area.light .selected-model:hover {
+ background: transparent;
+ padding: 4px 6px;
+ }
+}
diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js
new file mode 100644
index 000000000..4389b5f93
--- /dev/null
+++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js
@@ -0,0 +1,607 @@
+/* eslint-disable no-await-in-loop */
+
+import { createTag, getUnityLibs } from '../../../scripts/utils.js';
+
+function placeholderText(root, iconClass) {
+ const icon = root.querySelector(`.${iconClass}`) || root.querySelector(`[class*="${iconClass}"]`);
+ if (!icon) return '';
+ return (icon.closest('li')?.innerText || '').replace(/\s+/g, ' ').trim();
+}
+
+function labelForField(root, iconClass, fallback) {
+ return placeholderText(root, iconClass) || fallback;
+}
+
+function extractLegalFootFromAuthoring(root) {
+ const marker = root.querySelector('[class*="icon-legal-terms"]');
+ if (!marker) return null;
+ const li = marker.closest('li');
+ const foot = createTag('div', { class: 'pbu-legal-foot' });
+ if (li?.parentElement) {
+ while (li.firstChild) foot.append(li.firstChild);
+ li.remove();
+ return foot;
+ }
+ foot.append(marker.cloneNode(true));
+ marker.remove();
+ return foot;
+}
+
+function svgIcon(href) {
+ return ``;
+}
+
+function syncDropdownSelection(list, activeLink) {
+ list.querySelectorAll('li').forEach((li) => {
+ const a = li.querySelector('a');
+ const isActive = a === activeLink;
+ li.classList.toggle('selected', isActive);
+ a?.setAttribute('aria-selected', isActive ? 'true' : 'false');
+ });
+}
+
+function closeDropdown(container, triggerBtn, list) {
+ container.classList.remove('show-menu');
+ list.setAttribute('style', 'display: none;');
+ triggerBtn.setAttribute('aria-expanded', 'false');
+}
+
+function buildDropdownShell({ label, menuId, extraClass = '', imgEl = null, ariaLabelledBy = null }) {
+ const container = createTag('div', {
+ class: `models-container${extraClass ? ` ${extraClass}` : ''}`,
+ 'aria-label': label,
+ });
+
+ const nameContainer = createTag('span', { class: 'model-name' });
+ const menuIcon = createTag('span', { class: 'menu-icon' }, svgIcon('#unity-chevron-icon'));
+
+ const triggerBtn = createTag('button', {
+ type: 'button',
+ class: 'selected-model',
+ 'aria-expanded': 'false',
+ 'aria-controls': menuId,
+ 'aria-haspopup': 'listbox',
+ role: 'combobox',
+ });
+ if (imgEl) triggerBtn.append(imgEl, nameContainer, menuIcon);
+ else triggerBtn.append(nameContainer, menuIcon);
+
+ const listAttrs = { class: 'verb-list', id: menuId, role: 'listbox' };
+ if (ariaLabelledBy) listAttrs['aria-labelledby'] = ariaLabelledBy;
+ const list = createTag('ul', listAttrs);
+ list.setAttribute('style', 'display: none;');
+
+ container.append(triggerBtn, list);
+ return {
+ container, triggerBtn, nameContainer, menuIcon, list,
+ };
+}
+
+function attachDropdownBehavior(container, triggerBtn, list) {
+ const getOptions = () => [...list.querySelectorAll('a.model-link')];
+ const focusSelectedOrFirst = () => {
+ const options = getOptions();
+ if (!options.length) return;
+ const selected = options.find((option) => option.getAttribute('aria-selected') === 'true');
+ (selected || options[0])?.focus();
+ };
+
+ triggerBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ document.querySelectorAll('.models-container.show-menu').forEach((other) => {
+ if (other === container) return;
+ other.classList.remove('show-menu');
+ other.querySelector(':scope > .verb-list')?.setAttribute('style', 'display: none;');
+ other.querySelector('.selected-model')?.setAttribute('aria-expanded', 'false');
+ });
+ const isOpen = container.classList.toggle('show-menu');
+ if (isOpen) list.removeAttribute('style');
+ else list.setAttribute('style', 'display: none;');
+ triggerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
+ });
+
+ triggerBtn.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ closeDropdown(container, triggerBtn, list);
+ triggerBtn.focus();
+ return;
+ }
+
+ if (!['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(e.key)) return;
+ e.preventDefault();
+ const isOpen = container.classList.contains('show-menu');
+ if (!isOpen) {
+ container.classList.add('show-menu');
+ list.removeAttribute('style');
+ triggerBtn.setAttribute('aria-expanded', 'true');
+ }
+ focusSelectedOrFirst();
+ });
+
+ list.addEventListener('keydown', (e) => {
+ const options = getOptions();
+ if (!options.length) return;
+ const idx = options.findIndex((option) => option === document.activeElement);
+ if (e.key === 'Tab') {
+ if (idx < 0) return;
+ const atStart = idx === 0;
+ const atEnd = idx === options.length - 1;
+ if ((e.shiftKey && atStart) || (!e.shiftKey && atEnd)) {
+ closeDropdown(container, triggerBtn, list);
+ }
+ return;
+ }
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ closeDropdown(container, triggerBtn, list);
+ triggerBtn.focus();
+ return;
+ }
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ const next = idx < 0 ? 0 : (idx + 1) % options.length;
+ options[next]?.focus();
+ return;
+ }
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ const next = idx < 0 ? options.length - 1 : (idx - 1 + options.length) % options.length;
+ options[next]?.focus();
+ return;
+ }
+ if (e.key === 'Home') {
+ e.preventDefault();
+ options[0]?.focus();
+ return;
+ }
+ if (e.key === 'End') {
+ e.preventDefault();
+ options[options.length - 1]?.focus();
+ return;
+ }
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ const active = idx >= 0 ? options[idx] : options[0];
+ active?.click();
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ if (!container.contains(/** @type {Node} */ (e.target))) {
+ closeDropdown(container, triggerBtn, list);
+ }
+ });
+}
+
+export default class PromptBarUploadWidget {
+ constructor(target, el, workflowCfg, spriteCon) {
+ this.target = target;
+ this.el = el;
+ this.workflowCfg = workflowCfg;
+ this.spriteCon = spriteCon;
+ this.widgetWrap = null;
+ this.actionMap = {};
+ this.models = null;
+ this.aspectRatioMap = {};
+ this.sizeMap = {};
+ this.selectedModelId = '';
+ this.selectedAspectRatio = '';
+ this.lanaOptions = { sampleRate: 1, tags: 'Unity-FF-PBU' };
+ this.showAspectRatio = false;
+ this.showMore = false;
+ this.actionContainerEl = null;
+ }
+
+ async loadModels() {
+ const { origin } = window.location;
+ const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.'))
+ ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live`
+ : origin;
+ const res = await fetch(`${baseUrl}/unity/configs/prompt/model-picker-video.json`);
+ if (!res.ok) throw new Error('Failed to fetch video models.');
+ const json = await res.json();
+ this.models = json?.content?.data || [];
+ this.buildAspectRatioMap();
+ }
+
+ buildAspectRatioMap() {
+ this.aspectRatioMap = {};
+ this.sizeMap = {};
+ const parseList = (str) => {
+ const s = String(str);
+ try {
+ const parsed = JSON.parse(s);
+ if (Array.isArray(parsed)) return parsed.map(String);
+ } catch { /* fall through to comma-split */ }
+ return s.split(',').map((v) => v.trim()).filter(Boolean);
+ };
+ (this.models || []).forEach((item) => {
+ const raw = item['aspect-ratio'];
+ if (!item.id || !raw) return;
+ const ratios = parseList(raw);
+ this.aspectRatioMap[item.id] = ratios;
+ const widths = item.width != null ? parseList(item.width) : [];
+ const heights = item.height != null ? parseList(item.height) : [];
+ this.sizeMap[item.id] = ratios.map((_, i) => ({
+ width: Number(widths[i]) || null,
+ height: Number(heights[i]) || null,
+ }));
+ });
+ }
+
+ getAspectRatiosForModel(modelId) {
+ return this.aspectRatioMap[modelId] || [];
+ }
+
+ getSizeForAspectRatio(modelId, ratio) {
+ const sizes = this.sizeMap[modelId] || [];
+ const ratios = this.aspectRatioMap[modelId] || [];
+ const idx = ratios.indexOf(ratio);
+ return idx !== -1 ? sizes[idx] : null;
+ }
+
+ readFeatureFlags() {
+ this.showAspectRatio = !!this.el.querySelector('[class*="icon-show-aspect-ratio"]');
+ this.showMore = !!this.el.querySelector('[class*="icon-show-more"]');
+ }
+
+ buildModelPicker() {
+ if (!this.models?.length) return null;
+ const defaultModel = this.models.find((m) => m.default === 'true' || m.default === true) || this.models[0];
+ this.selectedModelId = defaultModel?.id || '';
+
+ const imgEl = defaultModel?.icon ? createTag('img', { src: defaultModel.icon, alt: '' }) : null;
+ const { container, triggerBtn, nameContainer, list } = buildDropdownShell({
+ label: 'Model options',
+ menuId: 'pbu-model-menu',
+ imgEl,
+ ariaLabelledBy: 'listbox-label',
+ });
+ nameContainer.textContent = (defaultModel?.name || '').trim();
+
+ this.models.forEach((model, idx) => {
+ const selectedIcon = createTag('span', { class: 'selected-icon' }, svgIcon('#unity-checkmark-icon'));
+ const nameSpan = createTag('span', { class: 'model-name' }, (model.name || model.id || '').trim());
+ const link = createTag('a', {
+ href: '#',
+ class: 'verb-link model-link',
+ 'data-model-id': model.id,
+ 'data-model-name': (model.name || '').trim(),
+ 'data-model-icon': model.icon || '',
+ ...(model.version != null && model.version !== '' ? { 'data-model-version': String(model.version) } : {}),
+ 'aria-selected': idx === 0 ? 'true' : 'false',
+ role: 'option',
+ });
+ link.append(selectedIcon);
+ if (model.icon) link.append(createTag('img', { src: model.icon, alt: '' }));
+ link.append(nameSpan);
+ const li = createTag('li', { class: `verb-item${idx === 0 ? ' selected' : ''}`, role: 'presentation' });
+ li.append(link);
+ list.append(li);
+ });
+
+ list.addEventListener('click', (e) => {
+ const link = e.target.closest('a.model-link');
+ if (!link) return;
+ e.preventDefault();
+ e.stopPropagation();
+ const modelId = link.getAttribute('data-model-id') || '';
+ const modelName = link.getAttribute('data-model-name') || '';
+ const modelIcon = link.getAttribute('data-model-icon') || '';
+ const modelVersion = link.getAttribute('data-model-version') || '';
+ this.selectedModelId = modelId;
+ nameContainer.textContent = modelName;
+ const triggerIcon = triggerBtn.querySelector(':scope > img');
+ if (modelIcon) {
+ if (triggerIcon) {
+ triggerIcon.setAttribute('src', modelIcon);
+ } else {
+ triggerBtn.prepend(createTag('img', { src: modelIcon, alt: '' }));
+ }
+ } else if (triggerIcon) {
+ triggerIcon.remove();
+ }
+ this.widgetWrap?.setAttribute('data-selected-model-id', modelId);
+ this.widgetWrap?.setAttribute('data-selected-model-name', modelName);
+ if (modelVersion) this.widgetWrap?.setAttribute('data-selected-model-version', modelVersion);
+ else this.widgetWrap?.removeAttribute('data-selected-model-version');
+ syncDropdownSelection(list, link);
+ closeDropdown(container, triggerBtn, list);
+ if (this.showAspectRatio) this.updateAspectRatioOptions(modelId);
+ });
+
+ triggerBtn.addEventListener('click', () => triggerBtn.dispatchEvent(new CustomEvent('pbu-model-dropdown-open', { bubbles: true })));
+ attachDropdownBehavior(container, triggerBtn, list);
+ this.widgetWrap?.setAttribute('data-selected-model-id', this.selectedModelId);
+ this.widgetWrap?.setAttribute('data-selected-model-name', (defaultModel?.name || '').trim());
+ if (defaultModel?.version != null && defaultModel.version !== '') {
+ this.widgetWrap?.setAttribute('data-selected-model-version', String(defaultModel.version));
+ } else {
+ this.widgetWrap?.removeAttribute('data-selected-model-version');
+ }
+ return container;
+ }
+
+ setSelectedAspectRatio(modelId, ratio) {
+ this.selectedAspectRatio = ratio;
+ this.widgetWrap?.setAttribute('data-selected-aspect-ratio', ratio);
+ const size = this.getSizeForAspectRatio(modelId, ratio);
+ if (size?.width) this.widgetWrap?.setAttribute('data-selected-width', size.width);
+ else this.widgetWrap?.removeAttribute('data-selected-width');
+ if (size?.height) this.widgetWrap?.setAttribute('data-selected-height', size.height);
+ else this.widgetWrap?.removeAttribute('data-selected-height');
+ }
+
+ syncDefaultAttributes() {
+ if (!this.widgetWrap || !this.selectedModelId) return;
+ const defaultModel = this.models?.find((m) => m.id === this.selectedModelId);
+ this.widgetWrap.setAttribute('data-selected-model-id', this.selectedModelId);
+ this.widgetWrap.setAttribute('data-selected-model-name', (defaultModel?.name || '').trim());
+ if (defaultModel?.version != null && defaultModel.version !== '') {
+ this.widgetWrap.setAttribute('data-selected-model-version', String(defaultModel.version));
+ } else {
+ this.widgetWrap.removeAttribute('data-selected-model-version');
+ }
+ if (this.selectedAspectRatio) {
+ this.setSelectedAspectRatio(this.selectedModelId, this.selectedAspectRatio);
+ }
+ }
+
+ buildAspectRatioDropdown(modelId) {
+ const ratios = this.getAspectRatiosForModel(modelId);
+ if (!ratios.length) return null;
+ this.setSelectedAspectRatio(modelId, ratios[0]);
+
+ const { container, triggerBtn, nameContainer, list } = buildDropdownShell({
+ label: 'Aspect ratio',
+ menuId: 'pbu-aspect-menu',
+ extraClass: 'pbu-aspect-models',
+ });
+ nameContainer.textContent = ratios[0];
+
+ ratios.forEach((ratio, idx) => {
+ const selectedIcon = createTag('span', { class: 'selected-icon' }, svgIcon('#unity-checkmark-icon'));
+ const link = createTag('a', {
+ href: '#',
+ class: 'verb-link model-link',
+ 'data-ratio': ratio,
+ 'aria-selected': idx === 0 ? 'true' : 'false',
+ role: 'option',
+ });
+ link.append(selectedIcon, createTag('span', { class: 'model-name' }, ratio));
+ const li = createTag('li', { class: `verb-item${idx === 0 ? ' selected' : ''}`, role: 'presentation' });
+ li.append(link);
+ list.append(li);
+ });
+
+ list.addEventListener('click', (e) => {
+ const link = e.target.closest('a.model-link');
+ if (!link) return;
+ e.preventDefault();
+ e.stopPropagation();
+ const ratio = link.getAttribute('data-ratio') || '';
+ nameContainer.textContent = ratio;
+ this.setSelectedAspectRatio(modelId, ratio);
+ syncDropdownSelection(list, link);
+ closeDropdown(container, triggerBtn, list);
+ });
+
+ triggerBtn.addEventListener('click', () => triggerBtn.dispatchEvent(new CustomEvent('pbu-ratio-dropdown-open', { bubbles: true })));
+ attachDropdownBehavior(container, triggerBtn, list);
+ return container;
+ }
+
+ updateAspectRatioOptions(modelId) {
+ const ac = this.actionContainerEl ?? this.widgetWrap?.querySelector('.action-container');
+ ac?.querySelector('.pbu-aspect-models')?.remove();
+ const picker = this.buildAspectRatioDropdown(modelId);
+ if (!picker || !ac) return;
+ const modelPicker = ac.querySelector('.models-container:not(.pbu-aspect-models)');
+ if (modelPicker) modelPicker.after(picker);
+ else ac.append(picker);
+ }
+
+ buildLeftSection() {
+ const leftSectionLabel = placeholderText(this.el, 'icon-dropzone-label');
+ const uploadLabel = createTag('div', { class: 'unity-slf-copy-label pbu-upload-heading' }, leftSectionLabel);
+ const { wrap: dropZoneWrap, ...dropZoneRefs } = this.buildDropZone();
+ const leftSection = createTag('div', { class: 'pbu-left-section' });
+ leftSection.append(uploadLabel, dropZoneWrap);
+ return { leftSection, dropZoneRefs };
+ }
+
+ buildRightSection() {
+ const promptHeading = placeholderText(this.el, 'icon-placeholder-prompt')
+ || labelForField(this.el, 'icon-label-prompt', 'Prompt');
+ const promptLabel = createTag('label', {
+ for: 'pbuPromptInput',
+ class: 'unity-slf-copy-label unity-slf-prompt-label',
+ }, promptHeading);
+
+ const promptTextarea = this.buildPromptTextarea();
+
+ const actionContainer = createTag('div', { class: 'action-container' });
+ this.actionContainerEl = actionContainer;
+
+ if (this.models?.length) {
+ const mp = this.buildModelPicker();
+ if (mp) actionContainer.append(mp);
+ }
+ if (this.showAspectRatio && this.selectedModelId) {
+ const ar = this.buildAspectRatioDropdown(this.selectedModelId);
+ if (ar) actionContainer.append(ar);
+ }
+ if (this.showMore) {
+ const moreBtn = this.buildMoreButton();
+ if (moreBtn) actionContainer.append(moreBtn);
+ }
+
+ const actWrap = createTag('div', { class: 'act-wrap' });
+ actWrap.append(this.buildGenerateButton());
+
+ const controlsFooter = createTag('div', { class: 'pbu-controls-footer' });
+ controlsFooter.append(actionContainer, actWrap);
+
+ const promptBarContainer = createTag('div', { class: 'pbu-prompt-bar-container' });
+ promptBarContainer.append(promptLabel, promptTextarea, controlsFooter);
+
+ const rightSection = createTag('div', { class: 'pbu-right-section' });
+ rightSection.append(promptBarContainer);
+ return rightSection;
+ }
+
+ buildDropZone() {
+ const allowedFileTypes = this.workflowCfg?.targetCfg?.limits?.allowedFileTypes;
+ const fileInput = createTag('input', {
+ type: 'file',
+ id: 'file-upload',
+ accept: allowedFileTypes.join(','),
+ hidden: '',
+ 'aria-hidden': 'true',
+ });
+
+ const dropContent = createTag('div', { class: 'pbu-drop-content' });
+ dropContent.append(createTag('img', { loading: 'lazy', src: `${getUnityLibs()}/img/icons/upload.svg` }));
+ const dropZone = createTag('div', {
+ class: 'drop-zone',
+ role: 'button',
+ tabindex: '0',
+ 'aria-label': 'Upload image',
+ });
+ dropZone.append(fileInput, dropContent);
+ dropZone.addEventListener('keydown', (e) => {
+ if (e.key !== 'Enter' && e.key !== ' ') return;
+ e.preventDefault();
+ fileInput.click();
+ });
+ const selectSpinner = createTag('div', { class: 'pbu-select-spinner hidden', 'aria-hidden': 'true', role: 'status' });
+ selectSpinner.append(createTag('div', { class: 'pbu-select-spinner-ring' }));
+
+ const previewImg = createTag('img', { class: 'pbu-preview-img', alt: 'Selected image preview' });
+ const deleteBtn = createTag('button', { type: 'button', class: 'pbu-delete-btn', 'aria-label': 'Remove image' });
+ deleteBtn.innerHTML = svgIcon('#unity-trash-icon');
+ const uploadSpinner = createTag('div', { class: 'pbu-spinner hidden', 'aria-label': 'Uploading', role: 'status' });
+ const preview = createTag('div', { class: 'pbu-preview hidden', 'aria-hidden': 'true' });
+ preview.append(previewImg, deleteBtn, uploadSpinner);
+
+ const wrap = createTag('div', { class: 'pbu-drop-zone-wrap' });
+ wrap.append(dropZone, selectSpinner, preview);
+ return { wrap, dropZone, preview, previewImg, deleteBtn,};
+ }
+
+ buildPromptTextarea() {
+ const defaultPrompt = placeholderText(this.el, 'icon-default-prompt') || '';
+ const maxCharLimit = this.workflowCfg?.targetCfg?.limits?.['max-char-limit'] ?? 750;
+ const textarea = createTag('textarea', {
+ id: 'pbuPromptInput',
+ class: 'inp-field',
+ rows: '1',
+ maxlength: String(maxCharLimit),
+ 'aria-label': defaultPrompt,
+ 'aria-autocomplete': 'list',
+ });
+ textarea.value = defaultPrompt;
+ textarea.addEventListener('input', () => textarea.dispatchEvent(new CustomEvent('pbu-enter-prompt', { bubbles: true })), { once: true });
+ return textarea;
+ }
+
+ buildGenerateButton() {
+ const generateLi = this.el.querySelector('[class*="icon-generate"]')?.closest('li');
+ const genBtnText = (generateLi?.innerText).trim().split('\n')[0] || 'Generate';
+ const img = generateLi?.querySelector('img[src*=".svg"]');
+ const btn = createTag('a', { href: '#', class: 'unity-act-btn gen-btn', 'daa-ll': 'Generate-video', 'aria-label': genBtnText });
+ if (img) {
+ img.setAttribute('alt', 'Generate video');
+ btn.append(createTag('div', { class: 'btn-ico' }, img));
+ }
+ if (genBtnText) btn.append(createTag('div', { class: 'btn-txt' }, genBtnText.split('\n')[0]));
+ return btn;
+ }
+
+ buildMoreButton() {
+ if (!this.showMore) return null;
+ const moreLi = this.el.querySelector('[class*="icon-more"]')?.closest('li');
+ const txt = (moreLi?.innerText || 'More').trim().split('\n')[0] || 'More';
+ const btn = createTag('a', { href: '#', class: 'unity-act-btn pbu-more-btn more-btn', 'aria-label': txt });
+ btn.append(
+ createTag('span', { class: 'btn-ico' }, svgIcon('#unity-more-icon')),
+ createTag('div', { class: 'btn-txt' }, txt),
+ );
+ btn.addEventListener('click', () => btn.dispatchEvent(new CustomEvent('pbu-more-click', { bubbles: true })));
+ return btn;
+ }
+
+ addWidget() {
+ const interactArea = this.target?.querySelector('.copy');
+ const { target: anchorSelector, insert } = this.workflowCfg.targetCfg || {};
+ const para = anchorSelector ? interactArea?.querySelector(anchorSelector) : null;
+ if (para && insert === 'before') para.before(this.widgetWrap);
+ else if (para) para.after(this.widgetWrap);
+ else interactArea?.appendChild(this.widgetWrap);
+ }
+
+
+ wireImagePreview({ dropZone, preview, previewImg, deleteBtn }) {
+ const showPreview = (file) => {
+ const url = URL.createObjectURL(file);
+ previewImg.src = url;
+ previewImg.onload = () => URL.revokeObjectURL(url);
+ dropZone.classList.add('hidden');
+ dropZone.setAttribute('aria-hidden', 'true');
+ preview.classList.remove('hidden');
+ preview.removeAttribute('aria-hidden');
+ };
+
+ const showDropZone = () => {
+ dropZone.classList.remove('hidden');
+ dropZone.removeAttribute('aria-hidden');
+ preview.classList.add('hidden');
+ preview.setAttribute('aria-hidden', 'true');
+ previewImg.src = '';
+ };
+
+ this.widgetWrap?.addEventListener('pbu-image-selected', (e) => showPreview(e.detail.file));
+ deleteBtn?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ showDropZone();
+ this.widgetWrap?.dispatchEvent(new CustomEvent('pbu-delete-image'));
+ });
+ }
+
+ async initWidget() {
+ this.readFeatureFlags();
+
+ try {
+ await this.loadModels();
+ } catch (e) {
+ window.lana?.log(`Message: Failed to load video models, Error: ${e}`, this.lanaOptions);
+ }
+
+ const { leftSection, dropZoneRefs } = this.buildLeftSection();
+ const rightSection = this.buildRightSection();
+ const main = createTag('div', { class: 'pbu-main' });
+ main.append(leftSection, rightSection);
+ const skin = this.el.classList.contains('light') ? 'light' : 'dark';
+ const interactiveShell = createTag('div', { class: `interactive-area ${skin}` });
+ interactiveShell.append(main);
+ const root = createTag('div', { class: 'unity-prompt-bar-upload unity-enabled' });
+ root.append(interactiveShell);
+ const holder = createTag('div', { class: 'unity-pbu-config-holder unity-slf-sr-only' });
+ holder.setAttribute('aria-hidden', 'true');
+ while (this.el.firstChild) holder.append(this.el.firstChild);
+ this.el.append(holder);
+ this.el.classList.add('unity-prompt-bar-upload-host');
+ const unitySprite = createTag('div', { class: 'unity-sprite-container' });
+ unitySprite.innerHTML = this.spriteCon || '';
+ const legalFoot = extractLegalFootFromAuthoring(this.el);
+ this.widgetWrap = createTag('div', { class: 'ex-unity-wrap verb-options pbu-widget' });
+ this.widgetWrap.append(unitySprite, root);
+ if (legalFoot) this.widgetWrap.append(legalFoot);
+ this.syncDefaultAttributes();
+
+ this.addWidget();
+ this.wireImagePreview(dropZoneRefs);
+ return this.workflowCfg.targetCfg.actionMap;
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js
new file mode 100644
index 000000000..3b43cbced
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/action-binder.js
@@ -0,0 +1,733 @@
+/* eslint-disable max-len */
+/* eslint-disable max-classes-per-file */
+/* eslint-disable no-await-in-loop */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-restricted-syntax */
+
+import {
+ unityConfig,
+ getUnityLibs,
+ priorityLoad,
+ createTag,
+ getLocale,
+ getLibs,
+ getHeaders,
+ getApiCallOptions,
+ sendAnalyticsEvent,
+} from '../../../scripts/utils.js';
+
+function normalizeToArray(value) {
+ if (value == null) return [];
+ if (Array.isArray(value)) return value.filter(Boolean);
+ if (typeof value.forEach === 'function' && typeof value.length === 'number') {
+ try { return [...value]; } catch { return [value]; }
+ }
+ return [value];
+}
+
+class ServiceHandler {
+ constructor(renderWidget = false, canvasArea = null, unityEl = null, workflowCfg = {}, getAdditionalHeaders = null) {
+ this.renderWidget = renderWidget;
+ this.canvasArea = canvasArea;
+ this.unityEl = unityEl;
+ this.workflowCfg = workflowCfg;
+ this.getAdditionalHeaders = getAdditionalHeaders;
+ }
+
+ async postCallToService(api, options, failOnError = true) {
+ const postOpts = {
+ method: 'POST',
+ headers: await getHeaders(unityConfig.apiKey, this.getAdditionalHeaders?.() || {}),
+ ...options,
+ };
+ let response;
+ try {
+ response = await fetch(api, postOpts);
+ } catch (e) {
+ if (e instanceof TypeError) {
+ const error = new Error(`Network error. URL: ${api}; Error message: ${e.message}`);
+ error.status = 0;
+ throw error;
+ }
+ throw e;
+ }
+ if (failOnError && response.status !== 200) {
+ const error = new Error('Operation failed');
+ error.status = response.status;
+ throw error;
+ }
+ if (!failOnError) return response;
+ return response.json();
+ }
+
+ showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') {
+ sendAnalyticsEvent(new CustomEvent(`Upload ${errorType} error|UnityWidget|${errorCallbackOptions.errorCode || ''}|${JSON.stringify(errorCallbackOptions.fileMetaData) || ''}`));
+ if (!errorCallbackOptions.errorToastEl) return;
+ const msg = this.unityEl.querySelector(errorCallbackOptions.errorType)?.closest('li')?.textContent?.trim();
+ this.canvasArea.forEach((element) => {
+ element.style.pointerEvents = 'none';
+ const errorToast = element.querySelector('.alert-holder');
+ if (!errorToast) return;
+ const closeBtn = errorToast.querySelector('.alert-close');
+ if (closeBtn) closeBtn.style.pointerEvents = 'auto';
+ const alertText = errorToast.querySelector('.alert-text p');
+ if (!alertText) return;
+ alertText.innerText = msg;
+ errorToast.classList.add('show');
+ });
+ window.lana?.log(`Message: ${msg}, Error: ${error || ''}`, lanaOptions);
+ }
+}
+
+export default class ActionBinder {
+ constructor(unityEl, workflowCfg, block, canvasArea, actionMap = {}) {
+ this.unityEl = unityEl;
+ this.workflowCfg = workflowCfg;
+ this.block = block;
+ this.canvasArea = canvasArea;
+ this.actionMap = actionMap;
+ this.errorToastEl = null;
+ this.transitionScreen = null;
+ this.LOADER_LIMIT = 95;
+ this.serviceHandler = null;
+ this.uploadAbortController = null;
+ this.assetId = null;
+ this.pendingFile = null;
+ this.filesData = {};
+ this.sendAnalyticsToSplunk = null;
+ this.analyticsModule = null;
+ this.promiseStack = [];
+ this.desktop = false;
+ this.toastCanvasAreas = normalizeToArray(canvasArea);
+ this.apiConfig = this.getApiConfig();
+ this.verb = this.getVerbFromDom();
+ this.initActionListeners = this.initActionListeners.bind(this);
+ const searchRoot = canvasArea || block;
+ this.widgetWrap = searchRoot?.querySelector?.('.ex-unity-wrap') ?? searchRoot;
+ this.inputField = searchRoot?.querySelector?.('.inp-field');
+ this.limits = workflowCfg.targetCfg?.limits || {};
+ const productTag = workflowCfg.targetCfg?.[`productTag-${workflowCfg.productName?.toLowerCase()}`] || 'FF';
+ this.lanaOptions = { sampleRate: 1, tags: `Unity-${productTag}-PBU` };
+
+ }
+
+ getApiConfig() {
+ unityConfig.endPoint = {
+ assetUpload: `${unityConfig.apiEndPoint}/asset`,
+ acmpCheck: `${unityConfig.apiEndPoint}/asset/finalize`,
+ };
+ return unityConfig;
+ }
+
+ getAdditionalHeaders() {
+ const baseAction = this.workflowCfg?.supportedFeatures?.values()?.next()?.value;
+ const xUnityAction = this.verb ? `${baseAction}-${this.verb}` : baseAction;
+ return {
+ 'x-unity-product': this.workflowCfg?.productName,
+ 'x-unity-action': xUnityAction,
+ };
+ }
+
+ getVerbFromDom() {
+ const verbEl = this.unityEl?.querySelector('[class*="icon-operation-"]');
+ if (verbEl) {
+ const verbClass = Array.from(verbEl.classList).find((cls) => cls.startsWith('icon-operation-'));
+ const fromDom = verbClass?.slice('icon-operation-'.length);
+ if (fromDom) return fromDom;
+ }
+ return this.workflowCfg?.enabledFeatures?.[0];
+ }
+
+ async initAnalytics() {
+ if (this.analyticsModule) return;
+ this.analyticsModule = await import(`${getUnityLibs()}/scripts/analytics.js`);
+ if (this.workflowCfg.targetCfg?.sendSplunkAnalytics) {
+ this.sendAnalyticsToSplunk = this.analyticsModule.default;
+ }
+ }
+
+ logAnalytics(eventName, data) {
+ this.sendAnalyticsToSplunk?.(
+ eventName,
+ this.workflowCfg.productName,
+ { ...data, operation: this.verb },
+ `${unityConfig.apiEndPoint}/log`,
+ true,
+ );
+ }
+
+ resetUploadedAssetState({ dropPendingImage = false } = {}) {
+ this.uploadAbortController?.abort();
+ this.uploadAbortController = null;
+ this.assetId = null;
+ if (dropPendingImage) {
+ this.pendingFile = null;
+ this.filesData = {};
+ }
+ }
+
+ async createErrorToast() {
+ try {
+ const [alertImg, closeImg] = await Promise.all([
+ fetch(`${getUnityLibs()}/img/icons/alert.svg`).then((res) => res.text()),
+ fetch(`${getUnityLibs()}/img/icons/close.svg`).then((res) => res.text()),
+ ]);
+ const { decorateDefaultLinkAnalytics } = await import(`${getLibs()}/martech/attributes.js`);
+ this.toastCanvasAreas.forEach((canvasEl) => {
+ const mount = canvasEl.querySelector('.pbu-main') || canvasEl;
+ const alertText = createTag('div', { class: 'alert-text' }, createTag('p', {}, 'Alert Text'));
+ const alertIcon = createTag('div', { class: 'alert-icon' });
+ alertIcon.innerHTML = alertImg;
+ alertIcon.append(alertText);
+ const alertClose = createTag('a', { class: 'alert-close', href: '#' });
+ alertClose.innerHTML = closeImg;
+ alertClose.append(createTag('span', { class: 'alert-close-text' }, 'Close error toast'));
+ const alertContent = createTag('div', { class: 'alert-content' });
+ alertContent.append(alertIcon, alertClose);
+ const alertToast = createTag('div', { class: 'alert-toast' }, alertContent);
+ const errholder = createTag('div', { class: 'alert-holder' }, alertToast);
+ alertClose.addEventListener('click', (e) => {
+ this.preventDefault(e);
+ errholder.classList.remove('show');
+ canvasEl.style.pointerEvents = 'auto';
+ });
+ decorateDefaultLinkAnalytics(errholder);
+ mount.append(errholder);
+ });
+ return this.toastCanvasAreas[0]?.querySelector('.pbu-main .alert-holder')
+ || this.toastCanvasAreas[0]?.querySelector('.alert-holder');
+ } catch (e) {
+ window.lana?.log(`Message: Error creating error toast, Error: ${e}`, this.lanaOptions);
+ return null;
+ }
+ }
+
+ extractFiles(e) {
+ const files = [];
+ if (e.dataTransfer?.items) {
+ [...e.dataTransfer.items].forEach((item) => { if (item.kind === 'file') files.push(item.getAsFile()); });
+ } else if (e.target?.files) {
+ [...e.target.files].forEach((file) => files.push(file));
+ }
+ return files;
+ }
+
+ handleClientError(errorTypeSelector, errorCode, message = '') {
+ this.serviceHandler.showErrorToast(
+ {
+ errorToastEl: this.errorToastEl,
+ errorType: errorTypeSelector,
+ errorCode,
+ fileMetaData: this.filesData,
+ },
+ message,
+ this.lanaOptions,
+ 'client',
+ );
+ this.logAnalytics('Upload client error|UnityWidget', { errorData: { code: errorCode }, fileMetaData: this.filesData });
+ }
+
+ setSelectSpinnerVisible(visible) {
+ const wrap = this.widgetWrap?.querySelector('.pbu-drop-zone-wrap');
+ const el = wrap?.querySelector('.pbu-select-spinner');
+ if (!el) return;
+ el.classList.toggle('hidden', !visible);
+ el.setAttribute('aria-hidden', visible ? 'false' : 'true');
+ wrap?.classList.toggle('pbu-select-processing', !!visible);
+ }
+
+ async validateAndStoreFile(files) {
+ this.setSelectSpinnerVisible(true);
+ try {
+ if (!files?.length) return false;
+ if (files.length > (this.limits.maxNumFiles || 1)) {
+ this.handleClientError('.icon-error-filecount', 'error-filecount');
+ return false;
+ }
+ const file = files[0];
+ this.filesData = { count: files.length, size: file.size, type: file.type };
+ if (this.limits.allowedFileTypes && !this.limits.allowedFileTypes.includes(file.type)) {
+ this.handleClientError('.icon-error-filetype', 'error-filetype');
+ return false;
+ }
+ if (this.limits.maxFileSize && file.size > this.limits.maxFileSize) {
+ this.handleClientError('.icon-error-filesize', 'error-filesize');
+ return false;
+ }
+ this.resetUploadedAssetState();
+ this.pendingFile = file;
+ this.widgetWrap?.dispatchEvent(new CustomEvent('pbu-image-selected', { detail: { file } }));
+ return true;
+ } finally {
+ this.setSelectSpinnerVisible(false);
+ }
+ }
+
+ async uploadImgToUnity(storageUrl, _id, blobData, fileType, signal) {
+ const uploadOptions = {
+ method: 'PUT',
+ headers: { 'Content-Type': fileType },
+ body: blobData,
+ ...(signal && { signal }),
+ };
+ let response;
+ try {
+ response = await fetch(storageUrl, uploadOptions);
+ } catch (e) {
+ if (e instanceof TypeError) {
+ const error = new Error(`Network error. URL: ${storageUrl}; Error message: ${e.message}`);
+ error.status = 0;
+ throw error;
+ }
+ throw e;
+ }
+ if (response.status !== 200) {
+ const error = new Error('Failed to upload image to Unity');
+ error.status = response.status;
+ throw error;
+ }
+ }
+
+ async uploadAsset(file) {
+ const assetDetails = {
+ targetProduct: this.workflowCfg.productName,
+ name: file.name,
+ size: file.size,
+ format: file.type,
+ };
+ this.uploadAbortController = new AbortController();
+ const { signal } = this.uploadAbortController;
+ try {
+ const resJson = await this.serviceHandler.postCallToService(
+ this.apiConfig.endPoint.assetUpload,
+ { body: JSON.stringify(assetDetails) },
+ );
+ const { id, href, blocksize, uploadUrls } = resJson;
+ this.assetId = id;
+ this.logAnalytics('Asset Created|UnityWidget', { assetId: this.assetId });
+ const { default: UploadHandler } = await import(`${getUnityLibs()}/core/workflow/workflow-upload/upload-handler.js`);
+ const uploadHandler = new UploadHandler(this, this.serviceHandler);
+ if (blocksize && uploadUrls && Array.isArray(uploadUrls)) {
+ const { failedChunks, attemptMap } = await uploadHandler.uploadChunksToUnity(uploadUrls, file, blocksize, signal);
+ if (failedChunks?.size > 0) {
+ if (signal.aborted) return false;
+ const error = new Error(`One or more chunks failed for asset: ${id}`);
+ error.status = 504;
+ this.logAnalytics('Chunked Upload Failed|UnityWidget', {
+ assetId: this.assetId,
+ failedChunks: failedChunks.size,
+ maxRetryCount: Math.max(...Array.from(attemptMap.values())),
+ });
+ throw error;
+ }
+ await uploadHandler.scanImgForSafetyWithRetry(this.assetId, signal);
+ const { createChunkAnalyticsData } = await import(`${getUnityLibs()}/utils/chunkingUtils.js`);
+ const totalChunks = Math.ceil(file.size / blocksize);
+ this.logAnalytics(
+ 'Chunked Upload Completed|UnityWidget',
+ createChunkAnalyticsData('Chunked Upload Completed|UnityWidget', {
+ assetId: this.assetId,
+ chunkCount: totalChunks,
+ totalFileSize: file.size,
+ fileType: file.type,
+ }),
+ );
+ } else {
+ await this.uploadImgToUnity(href, id, file, file.type, signal);
+ await uploadHandler.scanImgForSafetyWithRetry(this.assetId, signal);
+ this.logAnalytics('Upload Completed|UnityWidget', { assetId: this.assetId });
+ }
+ return true;
+ } catch (e) {
+ if (signal.aborted || e.name === 'AbortError') {
+ window.lana?.log(`Message: Upload aborted, Error: ${e.message}`, this.lanaOptions);
+ return false;
+ }
+ this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, e, this.lanaOptions);
+ this.logAnalytics('Upload server error|UnityWidget', {
+ errorData: { code: 'error-request', subCode: `uploadAsset ${e.status}`, desc: e.message },
+ assetId: this.assetId,
+ });
+ return false;
+ }
+ }
+
+ validateInput(query) {
+ const maxCharLimit = this.limits?.['max-char-limit'] ?? 750;
+ if (query.length > maxCharLimit) {
+ this.handleClientError('.icon-error-max-length', 'max-prompt-characters-exceeded', 'Prompt too long');
+ this.logAnalytics('generate', { errorData: { code: 'max-prompt-characters-exceeded' } });
+ return false;
+ }
+ return true;
+ }
+
+ async ensureTransitionScreen() {
+ if (!this.transitionScreen) {
+ const { default: TransitionScreen } = await import(`${getUnityLibs()}/scripts/transition-screen.js`);
+ this.transitionScreen = new TransitionScreen(null, this.initActionListeners, this.LOADER_LIMIT, this.workflowCfg, this.desktop);
+ }
+ if (!this.transitionScreen.splashScreenEl) {
+ await this.transitionScreen.loadSplashFragment();
+ }
+ }
+
+ async handleGenerate(connectorGenerate = true) {
+ this.promiseStack = [];
+ if (!this.analyticsModule) await this.initAnalytics();
+ const pbuEvents = this.analyticsModule.PROMPT_BAR_EVENTS;
+ const query = this.inputField?.value?.trim() || '';
+ if (!this.validateInput(query)) return;
+
+ const selectedModelId = this.widgetWrap?.getAttribute('data-selected-model-id') || '';
+ const selectedAspectRatio = this.widgetWrap?.getAttribute('data-selected-aspect-ratio') || '';
+ const selectedModelName = this.widgetWrap?.getAttribute('data-selected-model-name') || selectedModelId;
+ const ctaEventName = connectorGenerate ? pbuEvents.GENERATE_CTA : pbuEvents.MORE;
+ sendAnalyticsEvent(new CustomEvent(pbuEvents.UPLOAD_STARTED));
+ sendAnalyticsEvent(new CustomEvent(ctaEventName));
+ if (selectedModelName) sendAnalyticsEvent(new CustomEvent(pbuEvents.generateModel(selectedModelName)));
+ if (selectedAspectRatio) sendAnalyticsEvent(new CustomEvent(pbuEvents.ratioSelect(selectedAspectRatio)));
+ this.logAnalytics(pbuEvents.UPLOAD_STARTED, { fileMetaData: this.filesData });
+ this.logAnalytics(ctaEventName, {
+ ...(selectedModelName && {
+ modelGenEventName: pbuEvents.generateModel(selectedModelName),
+ }),
+ assetId: this.assetId,
+ aspectRatio: selectedAspectRatio,
+ hasImage: !!this.pendingFile,
+ });
+ const searchRoot = this.canvasArea || this.block;
+ const interactiveShell = searchRoot?.querySelector?.('.interactive-area');
+ this.workflowCfg.theme = interactiveShell?.classList.contains('dark') ? 'dark' : null;
+
+ await this.ensureTransitionScreen();
+ await this.transitionScreen.showSplashScreen(true);
+
+ if (this.pendingFile && !this.assetId) {
+ const uploadOk = await this.uploadAsset(this.pendingFile);
+ if (!uploadOk) {
+ await this.transitionScreen.showSplashScreen();
+ return;
+ }
+ }
+ await this.continueInApp(query, selectedModelId, selectedAspectRatio, connectorGenerate);
+ }
+
+
+ async continueInApp(query, modelId, aspectRatio, connectorGenerate = true) {
+ const { getCgenQueryParams } = await import(`${getUnityLibs()}/utils/cgen-utils.js`);
+ const queryParams = getCgenQueryParams(this.unityEl);
+ const modelVersion = this.widgetWrap?.getAttribute('data-selected-model-version') || '';
+ const selectedWidth = Number(this.widgetWrap?.getAttribute('data-selected-width')) || null;
+ const selectedHeight = Number(this.widgetWrap?.getAttribute('data-selected-height')) || null;
+ const size = (selectedWidth && selectedHeight) ? { width: selectedWidth, height: selectedHeight } : null;
+
+ const connectorBody = {
+ targetProduct: this.workflowCfg.productName,
+ ...(this.assetId && { assetId: this.assetId }),
+ ...(query && { query }),
+ payload: {
+ workflow: this.workflowCfg.supportedFeatures.values().next().value,
+ verb: this.verb,
+ action: 'asset-upload',
+ locale: getLocale(),
+ additionalQueryParams: queryParams,
+ size,
+ ...(modelId && { modelId }),
+ ...(modelVersion && { modelVersion }),
+ ...(aspectRatio && { aspectRatio }),
+ generate: connectorGenerate,
+ },
+ };
+ try {
+ const headerExtras = this.getAdditionalHeaders();
+ const postOpts = await getApiCallOptions(
+ 'POST',
+ unityConfig.apiKey,
+ headerExtras,
+ { body: JSON.stringify(connectorBody) },
+ );
+ const { default: NetworkUtils } = await import(`${getUnityLibs()}/utils/NetworkUtils.js`);
+ const networkUtils = new NetworkUtils();
+ const { url } = await networkUtils.fetchFromService(
+ this.apiConfig.connectorApiEndPoint,
+ postOpts,
+ async (response) => {
+ if (response.status !== 200) {
+ const error = new Error('Connector call failed');
+ error.status = response.status;
+ throw error;
+ }
+ return response.json();
+ },
+ );
+ this.logAnalytics('Generate Complete|UnityWidget', { assetId: this.assetId });
+ this.LOADER_LIMIT = 100;
+ if (this.transitionScreen?.splashScreenEl) {
+ this.transitionScreen.LOADER_LIMIT = 100;
+ this.transitionScreen.updateProgressBar(this.transitionScreen.splashScreenEl, 100);
+ }
+ if (url) window.location.href = url;
+ } catch (err) {
+ if (err.message === 'Operation termination requested.') return;
+ await this.transitionScreen?.showSplashScreen();
+ this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, err, this.lanaOptions);
+ this.logAnalytics('Generate Error|UnityWidget', {
+ errorData: { code: 'request-failed', subCode: err.status, desc: err.message },
+ assetId: this.assetId,
+ });
+ window.lana?.log(`Message: Connector call failed, Error: ${err}`, this.lanaOptions);
+ }
+ }
+
+ async handlePreloads() {
+ const parr = [];
+ if (this.workflowCfg.targetCfg?.showSplashScreen) {
+ parr.push(`${getUnityLibs()}/core/styles/splash-screen.css`);
+ }
+ if (parr.length) await priorityLoad(parr);
+ }
+
+ isStringActionMap(actMap) {
+ return actMap && typeof actMap === 'object' && Object.keys(actMap).length > 0
+ && Object.values(actMap).every((v) => typeof v === 'string');
+ }
+
+ async cancelUploadOperation() {
+ try {
+ this.uploadAbortController?.abort();
+ this.uploadAbortController = null;
+ sendAnalyticsEvent(new CustomEvent('Cancel|UnityWidget'));
+ this.logAnalytics('Cancel|UnityWidget', { assetId: this.assetId });
+ await this.ensureTransitionScreen();
+ await this.transitionScreen.showSplashScreen();
+ const e = new Error('Operation termination requested.');
+ const cancelPromise = Promise.reject(e);
+ cancelPromise.catch(() => {});
+ this.promiseStack.unshift(cancelPromise);
+ } catch (error) {
+ await this.transitionScreen?.showSplashScreen();
+ window.lana?.log(`Message: Error cancelling upload operation, Error: ${error}`, this.lanaOptions);
+ throw error;
+ }
+ }
+
+ async executeActionMaps(value) {
+ await this.handlePreloads();
+ if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast();
+ if (value === 'interrupt') await this.cancelUploadOperation();
+ }
+
+ async bindStringActionMap(b, actMap) {
+ const actions = {
+ A: (el, key) => {
+ el.addEventListener('click', async (e) => {
+ const action = actMap[key];
+ if (action !== 'redirect') e.preventDefault();
+ await this.executeActionMaps(action);
+ });
+ },
+ DIV: (el, key) => {
+ el.addEventListener('drop', async (e) => {
+ sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget'));
+ this.preventDefault(e);
+ const extracted = this.extractFiles(e);
+ this.filesData = { count: extracted.length, size: extracted[0]?.size, type: extracted[0]?.type };
+ this.logAnalytics('Drag and drop|UnityWidget', { assetId: this.assetId, fileMetaData: this.filesData });
+ await this.executeActionMaps(actMap[key], extracted);
+ });
+ el.addEventListener('click', () => {
+ sendAnalyticsEvent(new CustomEvent('Click Drag and drop|UnityWidget'));
+ });
+ },
+ INPUT: (el, key) => {
+ el.addEventListener('click', () => {
+ this.toastCanvasAreas.forEach((element) => {
+ const errHolder = element.querySelector('.alert-holder');
+ if (errHolder?.classList.contains('show')) {
+ element.style.pointerEvents = 'auto';
+ errHolder.classList.remove('show');
+ }
+ });
+ });
+ el.addEventListener('change', async (e) => {
+ const extracted = this.extractFiles(e);
+ this.filesData = { count: extracted.length, size: extracted[0]?.size, type: extracted[0]?.type };
+ this.logAnalytics('Click Drag and drop|UnityWidget', { assetId: this.assetId, fileMetaData: this.filesData });
+ await this.executeActionMaps(actMap[key], extracted);
+ e.target.value = '';
+ });
+ },
+ };
+ for (const [key] of Object.entries(actMap)) {
+ const elements = b.querySelectorAll(key);
+ if (elements?.length) {
+ elements.forEach((el) => {
+ const actionType = el.nodeName;
+ if (actions[actionType]) actions[actionType](el, key);
+ });
+ }
+ }
+ }
+
+ setupServiceHandler() {
+ this.serviceHandler = new ServiceHandler(
+ this.workflowCfg.targetCfg?.renderWidget,
+ this.toastCanvasAreas,
+ this.unityEl,
+ this.workflowCfg,
+ this.getAdditionalHeaders.bind(this),
+ );
+ }
+
+ async initActionListeners(b = this.block, actMap = this.actionMap) {
+ const searchRoot = this.canvasArea || this.block;
+ this.widgetWrap = searchRoot?.querySelector?.('.ex-unity-wrap') || this.widgetWrap;
+
+ this.setupServiceHandler();
+ await this.initAnalytics();
+
+ if (this.isStringActionMap(actMap)) {
+ await this.bindStringActionMap(b, actMap);
+ return;
+ }
+ if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast();
+ await this.handlePreloads();
+ this.inputField = this.widgetWrap?.querySelector('#pbuPromptInput')
+ || this.widgetWrap?.querySelector('.inp-field')
+ || this.inputField;
+ for (const [selector, actionsList] of Object.entries(actMap)) {
+ const elements = (this.widgetWrap || searchRoot)?.querySelectorAll(selector);
+ if (!elements?.length) continue;
+ elements.forEach((el) => {
+ if (el.dataset.pbuBound) return;
+ el.dataset.pbuBound = 'true';
+ this.bindElement(el, actionsList);
+ });
+ }
+ this.inputField?.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ (this.widgetWrap || searchRoot)?.querySelector('.gen-btn')?.click();
+ }
+ });
+
+ const pbuEvents = this.analyticsModule.PROMPT_BAR_EVENTS;
+ this.bindWidgetInteractionEvent('pbu-enter-prompt', pbuEvents.ENTER_PROMPT, 'enter-prompt');
+ this.bindWidgetInteractionEvent('pbu-model-dropdown-open', pbuEvents.MODEL_SELECT_DROPDOWN, 'open');
+ this.bindWidgetInteractionEvent('pbu-ratio-dropdown-open', pbuEvents.RATIO_DROPDOWN, 'open');
+ this.widgetWrap?.addEventListener('pbu-delete-image', () => this.resetUploadedAssetState({ dropPendingImage: true }));
+ this.bindOuterMarqueeDropTarget();
+ }
+
+ bindOuterMarqueeDropTarget() {
+ const outerMarquee = this.block?.querySelector('.upload-marquee-layout') || this.block;
+ const dropZone = this.widgetWrap?.querySelector('.drop-zone');
+ if (!outerMarquee || outerMarquee.dataset.pbuOuterDropBound === 'true') return;
+ outerMarquee.dataset.pbuOuterDropBound = 'true';
+
+ let dragDepth = 0;
+ const hasFilePayload = (e) => !!e?.dataTransfer?.types
+ && Array.from(e.dataTransfer.types).includes('Files');
+ const setDropzoneHighlight = (isOn) => dropZone?.classList.toggle('drag-over', !!isOn);
+
+ outerMarquee.addEventListener('dragenter', (e) => {
+ if (!hasFilePayload(e)) return;
+ e.preventDefault();
+ dragDepth += 1;
+ setDropzoneHighlight(true);
+ });
+
+ outerMarquee.addEventListener('dragover', (e) => {
+ if (!hasFilePayload(e)) return;
+ e.preventDefault();
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
+ setDropzoneHighlight(true);
+ });
+
+ outerMarquee.addEventListener('dragleave', (e) => {
+ if (!hasFilePayload(e)) return;
+ e.preventDefault();
+ dragDepth = Math.max(0, dragDepth - 1);
+ if (dragDepth === 0) setDropzoneHighlight(false);
+ });
+
+ outerMarquee.addEventListener('drop', async (e) => {
+ if (!hasFilePayload(e)) return;
+ e.preventDefault();
+ dragDepth = 0;
+ setDropzoneHighlight(false);
+ sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget'));
+ const files = this.extractFiles(e);
+ await this.executeAction('file-selected', outerMarquee, files);
+ });
+ }
+
+ bindElement(el, actionsList) {
+ const actions = Array.isArray(actionsList) ? actionsList : [actionsList];
+ const primaryAction = actions[0]?.actionType;
+
+ switch (el.nodeName) {
+ case 'A':
+ case 'BUTTON':
+ el.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await this.executeAction(primaryAction, el);
+ });
+ break;
+ case 'DIV':
+ el.addEventListener('dragover', (e) => { e.preventDefault(); el.classList.add('drag-over'); });
+ el.addEventListener('dragleave', () => el.classList.remove('drag-over'));
+ el.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ el.classList.remove('drag-over');
+ sendAnalyticsEvent(new CustomEvent('Drag and drop|UnityWidget'));
+ const files = this.extractFiles(e);
+ await this.executeAction(primaryAction, el, files);
+ });
+ el.addEventListener('click', () => { this.block?.querySelector('#file-upload')?.click(); });
+ break;
+ case 'INPUT':
+ el.addEventListener('change', async (e) => {
+ const files = this.extractFiles(e);
+ await this.executeAction(primaryAction, el, files);
+ e.target.value = '';
+ });
+ break;
+ default:
+ break;
+ }
+ }
+
+ async executeAction(actionType, el, files) {
+ try {
+ switch (actionType) {
+ case 'generate':
+ await this.handleGenerate(true);
+ break;
+ case 'more':
+ await this.handleGenerate(false);
+ break;
+ case 'file-selected':
+ await this.validateAndStoreFile(files);
+ break;
+ default:
+ break;
+ }
+ } catch (err) {
+ window.lana?.log(`Message: Action "${actionType}" failed, Error: ${err}`, this.lanaOptions);
+ }
+ }
+
+ bindWidgetInteractionEvent(domEventName, analyticsEventName, action) {
+ this.widgetWrap?.addEventListener(domEventName, () => {
+ sendAnalyticsEvent(new CustomEvent(analyticsEventName));
+ this.logAnalytics(analyticsEventName, { action });
+ });
+ }
+
+ preventDefault(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg
new file mode 100644
index 000000000..b196e215e
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-prompt-bar-upload/sprite.svg
@@ -0,0 +1,46 @@
+
diff --git a/unitylibs/core/workflow/workflow-prompt-bar-upload/target-config.json b/unitylibs/core/workflow/workflow-prompt-bar-upload/target-config.json
new file mode 100644
index 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..ae9c1620f 100644
--- a/unitylibs/core/workflow/workflow-upload/action-binder.js
+++ b/unitylibs/core/workflow/workflow-upload/action-binder.js
@@ -84,9 +84,7 @@ export default class ActionBinder {
this.splashScreenEl = null;
this.transitionScreen = null;
this.LOADER_LIMIT = 95;
- const commonLimits = workflowCfg.targetCfg.limits || {};
- const productLimits = workflowCfg.targetCfg[`limits-${workflowCfg.productName.toLowerCase()}`] || {};
- this.limits = { ...commonLimits, ...productLimits };
+ this.limits = ActionBinder.resolveLimits(workflowCfg);
this.promiseStack = [];
this.initActionListeners = this.initActionListeners.bind(this);
const productTag = workflowCfg.targetCfg[`productTag-${workflowCfg.productName.toLowerCase()}`] || 'UNKNOWN';
@@ -99,6 +97,18 @@ export default class ActionBinder {
this.uploadAbortController = null;
}
+ static resolveLimits(workflowCfg) {
+ const targetCfg = workflowCfg.targetCfg || {};
+ const commonLimits = targetCfg.limits || {};
+ const productLimits = targetCfg[`limits-${workflowCfg.productName?.toLowerCase()}`] || {};
+ const featureLimits = Array.from(workflowCfg.supportedFeatures || []).reduce((acc, feature) => ({
+ ...acc,
+ ...(targetCfg[`limits-${feature}`] || {}),
+ }), {});
+ const hasFeatureLimits = Object.keys(featureLimits).length > 0;
+ return { ...commonLimits, ...(hasFeatureLimits ? featureLimits : productLimits) };
+ }
+
getApiConfig() {
unityConfig.endPoint = {
assetUpload: `${unityConfig.apiEndPoint}/asset`,
@@ -403,6 +413,21 @@ export default class ActionBinder {
return { width, height };
}
+ async checkVideoDuration(file) {
+ const { getVideoDuration } = await import(`${getUnityLibs()}/utils/FileUtils.js`);
+ const duration = await getVideoDuration(file);
+ this.filesData = { ...this.filesData, duration };
+ if (this.limits.minDuration && duration < this.limits.minDuration) {
+ this.handleClientUploadError('.icon-error-videominduration', 'error-minVideoduration', 'Video is too short');
+ throw new Error('Video is too short');
+ }
+ if (this.limits.maxDuration && duration > this.limits.maxDuration) {
+ this.handleClientUploadError('.icon-error-videomaxduration', 'error-maxVideoduration', 'Video is too long');
+ throw new Error('Video is too long');
+ }
+ return duration;
+ }
+
async initAnalytics() {
if (!this.sendAnalyticsToSplunk && this.workflowCfg.targetCfg.sendSplunkAnalytics) {
this.sendAnalyticsToSplunk = (await import(`${getUnityLibs()}/scripts/analytics.js`)).default;
@@ -420,14 +445,14 @@ export default class ActionBinder {
this.logAnalyticsinSplunk('Upload client error|UnityWidget', { errorData: { code: errorCode }, fileMetaData: this.filesData, action: 'upload' });
}
- async uploadImage(files) {
+ async uploadFile(files) {
if (!files) return;
const file = files[0];
if (this.limits.maxNumFiles !== files.length) {
this.handleClientUploadError('.icon-error-filecount', 'error-filecount');
return;
}
- if (!this.limits.allowedFileTypes.includes(file.type)) {
+ if (!this.limits.allowedFileTypes?.includes(file.type)) {
this.handleClientUploadError('.icon-error-filetype', 'error-filetype');
return;
}
@@ -435,8 +460,12 @@ export default class ActionBinder {
this.handleClientUploadError('.icon-error-filesize', 'error-filesize');
return;
}
- try { await this.checkImageDimensions(file); } catch (error) {
- window.lana?.log(`Message: Error checking image dimensions, Error: ${error}`, this.lanaOptions);
+ const isVideo = file.type.startsWith('video/');
+ try {
+ if (isVideo) { await this.checkVideoDuration(file);}
+ else { await this.checkImageDimensions(file);}
+ } catch (error) {
+ window.lana?.log(`Message: Error checking file constraints, Error: ${error}`, this.lanaOptions);
return;
}
sendAnalyticsEvent(new CustomEvent('Uploading Started|UnityWidget'));
@@ -475,7 +504,7 @@ export default class ActionBinder {
switch (value) {
case 'upload':
this.promiseStack = [];
- await this.uploadImage(files);
+ await this.uploadFile(files);
break;
case 'interrupt':
await this.cancelUploadOperation();
@@ -519,6 +548,9 @@ export default class ActionBinder {
});
},
INPUT: (el, key) => {
+ if (this.limits.allowedFileTypes?.length) {
+ el.setAttribute('accept', this.limits.allowedFileTypes.join(','));
+ }
el.addEventListener('click', () => {
this.canvasArea.forEach((element) => {
const errHolder = element.querySelector('.alert-holder');
diff --git a/unitylibs/core/workflow/workflow-upload/target-config.json b/unitylibs/core/workflow/workflow-upload/target-config.json
index 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..8c6b9a24c 100644
--- a/unitylibs/core/workflow/workflow.js
+++ b/unitylibs/core/workflow/workflow.js
@@ -26,29 +26,20 @@ class WfInitiator {
return cls ? cls.replace(/^widget-/, '') : 'prompt-bar';
}
- static getWidgetRegistry() {
- const widgetBase = `${getUnityLibs()}/core/widgets`;
- return {
- 'prompt-bar': [`${widgetBase}/prompt-bar/prompt-bar.js`, `${widgetBase}/prompt-bar/prompt-bar.css`],
- 'prompt-bar-style': [
- `${widgetBase}/prompt-bar-style/prompt-bar-style.js`,
- `${widgetBase}/prompt-bar-style/prompt-bar-style.css`,
- ],
- };
+ static widgetPathsForName(name) {
+ const widgetBase = `${getUnityLibs()}/core/widgets/${name}`;
+ return [`${widgetBase}/${name}.js`, `${widgetBase}/${name}.css`];
}
getWidgetPaths() {
this.widgetName = this.getWidgetNameFromClass();
- const registry = WfInitiator.getWidgetRegistry();
- return registry[this.widgetName] || registry['prompt-bar'];
+ return WfInitiator.widgetPathsForName(this.widgetName);
}
static getWidgetPathsFromEl(el) {
- const registry = WfInitiator.getWidgetRegistry();
- if (!el) return registry['prompt-bar'];
- const cls = [...el.classList].find((c) => c.startsWith('widget-'));
- const rawName = cls ? cls.replace(/^widget-/, '') : 'prompt-bar';
- return registry[rawName] || registry['prompt-bar'];
+ const cls = el && [...el.classList].find((c) => c.startsWith('widget-'));
+ const name = cls ? cls.replace(/^widget-/, '') : 'prompt-bar';
+ return WfInitiator.widgetPathsForName(name);
}
async priorityLibFetch(workflowName) {
@@ -67,6 +58,10 @@ class WfInitiator {
],
'workflow-ai': [...bundledWidgetAssets],
'workflow-firefly': fireflyShared,
+ 'workflow-prompt-bar-upload': [
+ `${baseWfPath}/sprite.svg`,
+ ...this.getWidgetPaths(),
+ ],
};
const commonResources = [
`${baseWfPath}/target-config.json`,
@@ -101,7 +96,7 @@ class WfInitiator {
if (this.targetConfig.renderWidget) {
const widgetPath = (this.workflowCfg.name === 'workflow-photoshop' || this.workflowCfg.name === 'workflow-ai')
? `${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/widget.js`
- : WfInitiator.getWidgetRegistry()[this.widgetName][0];
+ : WfInitiator.widgetPathsForName(this.widgetName)[0];
const { default: UnityWidget } = await import(widgetPath);
const spriteContent = await spriteSvg.text();
unityWidgetObject = new UnityWidget(
@@ -267,6 +262,10 @@ class WfInitiator {
sfList: new Set([feature]),
psw,
},
+ 'workflow-prompt-bar-upload': {
+ productName: product || 'Firefly',
+ sfList: new Set([feature || 'image-to-video']),
+ },
'workflow-firefly': {
productName: 'Firefly',
sfList: new Set(['text-to-mage']),
diff --git a/unitylibs/img/icons/upload.svg b/unitylibs/img/icons/upload.svg
new file mode 100755
index 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..f982ddf34 100644
--- a/unitylibs/scripts/analytics.js
+++ b/unitylibs/scripts/analytics.js
@@ -1,10 +1,18 @@
-export const PROMPT_WITH_STYLE_EVENTS = {
+export const PROMPT_BAR_EVENTS = {
ENTER_PROMPT: 'Enter Prompt|UnityWidget',
MODEL_SELECT_DROPDOWN: 'Model Select Dropdown|UnityWidget',
GENERATE_CTA: 'Click on Generate CTA|UnityWidget',
MODULE_PICKER: 'Module Picker Select Dropdown|UnityWidget',
+ RATIO_DROPDOWN: 'Ratio Dropdown Select|UnityWidget',
+ MORE: 'More|UnityWidget',
+ UPLOAD_STARTED: 'Uploading started|UnityWidget',
+ UPLOAD_ERROR: 'Upload error|UnityWidget',
+ generateModel: (modelName) => `Generate ${modelName}|UnityWidget`,
+ ratioSelect: (ratio) => `${ratio}|UnityWidget`,
};
+export const PROMPT_WITH_STYLE_EVENTS = PROMPT_BAR_EVENTS;
+
export function styleSelectionGenerateEventName(styleIndexOneBased) {
return `Style ${styleIndexOneBased}|UnityWidget`;
}
@@ -28,7 +36,7 @@ function getSessionID() {
function createPayloadForSplunk(metaData) {
const {
eventName, product, errorData, redirectUrl, assetId, statusCode, verb, action, workflowStep, fileMetaData, operation,
- styleEventName, modelGenEventName,
+ styleEventName, modelGenEventName, aspectRatio, hasImage,
} = metaData;
return {
event: {
@@ -45,6 +53,8 @@ function createPayloadForSplunk(metaData) {
...(fileMetaData && { fileMetaData }),
...(styleEventName && { style: styleEventName }),
...(modelGenEventName && { model: modelGenEventName }),
+ ...(aspectRatio && { aspectRatio }),
+ ...(hasImage !== undefined && hasImage !== null && { hasImage }),
},
source: {
user_agent: navigator.userAgent,
diff --git a/unitylibs/scripts/transition-screen.js b/unitylibs/scripts/transition-screen.js
index 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/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);