diff --git a/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index 3f0ddd66..d86a0ea1 100644 --- a/test/core/workflow/workflow.firefly.test.js +++ b/test/core/workflow/workflow.firefly.test.js @@ -26,7 +26,13 @@ describe('Firefly Workflow Tests', () => { unityElement = document.querySelector('.unity'); workflowCfg = { name: 'workflow-firefly', - targetCfg: { renderWidget: true, insert: 'before', target: 'a:last-of-type' }, + targetCfg: { + renderWidget: true, + insert: 'before', + target: 'a:last-of-type', + limits: { 'max-char-limit': 750 }, + 'limits-prompt-bar-audio': { 'max-char-limit': 5000 }, + }, }; spriteContainer = ''; block = document.querySelector('.unity-enabled'); @@ -2279,9 +2285,7 @@ describe('Firefly Workflow Tests', () => { }); it('should pass empty object to logAnalytics when splunkData is omitted', async () => { - await testActionBinder.sendFireflyAnalytics(new CustomEvent('firefly-analytics', { - detail: { adobeEventName: 'Enter Prompt|UnityWidget' }, - })); + await testActionBinder.sendFireflyAnalytics(new CustomEvent('firefly-analytics', { detail: { adobeEventName: 'Enter Prompt|UnityWidget' } })); expect(logStub.calledOnceWithExactly('Enter Prompt|UnityWidget', {})).to.be.true; }); }); @@ -2758,7 +2762,7 @@ describe('Firefly Workflow Tests', () => { { label: 'Var A', url: 'https://u1' }, { label: 'Var B', url: 'https://u2' }, { label: 'Var C', url: 'https://u3' }, - { label: 'Var D', url: 'https://u4' } + { label: 'Var D', url: 'https://u4' }, ]); expect(pm.image).to.exist; expect(pm.image[0].variations).to.deep.equal([]); diff --git a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css new file mode 100644 index 00000000..8bbae74d --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.css @@ -0,0 +1,826 @@ +.unity-slf-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.unity.workflow-firefly.widget-prompt-bar-audio.unity-prompt-bar-audio-host { + position: relative; + min-height: 0; +} + +.unity-prompt-bar-audio { + width: 100%; + box-sizing: border-box; +} + +.unity-prompt-bar-audio .unity-paf-main { + display: flex; + flex-direction: column; + gap: 0; + align-items: stretch; + flex: 1; + min-width: 0; +} + +.unity-prompt-bar-audio .unity-slf-left { + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; + flex: 1; + background: #1b1b1b; + border-radius: 10px; + padding: 16px; + box-sizing: border-box; +} + +.unity-prompt-bar-audio .unity-slf-controls { + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; +} + +.unity-prompt-bar-audio .unity-paf-voice-section { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.unity-prompt-bar-audio .unity-paf-voice-heading { + margin: 18px 0 0; + padding-top: 16px; + border-top: 2px solid rgb(255 255 255 / 12%); +} + +.unity-prompt-bar-audio .unity-paf-voice-row { + display: flex; + flex-flow: row nowrap; + gap: 4.76px; + align-items: stretch; + justify-content: flex-start; + width: 100%; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; + padding-bottom: 2px; +} + +.unity-prompt-bar-audio .unity-paf-voice-row.unity-paf-voice-row-peek { + box-sizing: border-box; + width: calc(100% + 16px); + padding-right: 16px; + scroll-padding-right: 16px; +} + +.unity-prompt-bar-audio .unity-paf-voice-row.unity-paf-voice-row-peek.unity-paf-voice-row-peek-scrolled { + width: calc(100% + 32px); + transform: translateX(-16px); + padding-left: 16px; + padding-right: 16px; + scroll-padding-left: 16px; + scroll-padding-right: 16px; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile { + --unity-paf-voice-tile-width: 231px; + --paf-voice-tile-border-width: 1.78px; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 5px; + box-sizing: border-box; + width: var(--unity-paf-voice-tile-width); + min-width: var(--unity-paf-voice-tile-width); + max-width: var(--unity-paf-voice-tile-width); + flex: 0 0 var(--unity-paf-voice-tile-width); + padding: 14px; + border-radius: 9.51px; + border: var(--paf-voice-tile-border-width) solid transparent; + background: #292929; + backdrop-filter: blur(29.72px); + overflow: hidden; + cursor: pointer; + text-align: start; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:hover { + background: #333; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus { + outline: none; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible { + border-color: #274dea; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile.selected { + border-color: #274dea; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile.selected:hover { + background: #2f2f2f; +} + +.unity-prompt-bar-audio .unity-paf-voice-tile-text { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; + flex: 1 1 auto; +} + +.unity-prompt-bar-audio .unity-paf-voice-name { + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 16px; + font-weight: 800; + line-height: 0.98; + letter-spacing: -0.48px; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.unity-prompt-bar-audio .unity-paf-voice-desc { + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 1.2; + letter-spacing: 0.14px; + color: rgb(255 255 255 / 92%); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.unity-prompt-bar-audio .unity-paf-voice-player { + position: relative; + width: 37.17px; + height: 37.17px; + flex: 0 0 37.17px; +} + +@media (hover: hover) and (pointer: fine) { + .unity-prompt-bar-audio .unity-paf-voice-tile .unity-paf-voice-player { + opacity: 0; + transition: opacity 0.15s ease; + } + + .unity-prompt-bar-audio .unity-paf-voice-tile:hover .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile:focus-visible .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile[aria-pressed="true"] .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile.selected .unity-paf-voice-player, + .unity-prompt-bar-audio .unity-paf-voice-tile:has(.unity-paf-voice-player--buffering) .unity-paf-voice-player { + opacity: 1; + } +} + +.unity-prompt-bar-audio .unity-paf-progress-svg { + display: block; + width: 37.17px; + height: 37.17px; +} + +.unity-prompt-bar-audio .unity-paf-ring-bg { + stroke: rgb(255 255 255 / 22%); +} + +.unity-prompt-bar-audio .unity-paf-ring-fg { + stroke: #f8f8f8; + transition: stroke-dashoffset 0.05s linear; +} + +.unity-prompt-bar-audio .unity-paf-pp-center { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.unity-prompt-bar-audio .unity-paf-pp-center .unity-paf-pp-svg { + display: block; + width: 18px; + height: 18px; +} + +.unity-prompt-bar-audio .unity-paf-pp-center .unity-paf-pp-svg use { + filter: brightness(0) invert(1); +} + +.unity-prompt-bar-audio .unity-paf-voice-player-loading { + box-sizing: border-box; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + color: #f8f8f8; + pointer-events: none; +} + +.unity-prompt-bar-audio .unity-paf-voice-player-loading-svg { + display: block; + width: 100%; + height: 100%; +} + +.unity-prompt-bar-audio .unity-paf-voice-player-loading-circle { + stroke-dasharray: 125.664; + stroke-dashoffset: 125.664; + animation: unity-paf-voice-player-loading-sweep 1s ease-in-out infinite; +} + +@keyframes unity-paf-voice-player-loading-sweep { + to { + stroke-dashoffset: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .unity-prompt-bar-audio .unity-paf-voice-player-loading-circle { + animation: none; + stroke-dashoffset: 62.8; + } +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + margin: 4px 0 0; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 12px; + line-height: 16px; + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot-line { + margin: 0; + max-width: 100%; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner { + box-sizing: border-box; + width: 100%; + flex-shrink: 0; + margin: 24px 0 0; + padding: 0 8px; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 12px; + line-height: 18px; + color: #fff; +} + +.unity-prompt-bar-audio:has(.interactive-area.light) .unity-paf-terms-banner { + color: rgb(46 46 46 / 92%); +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line { + margin: 0 auto; + max-width: 56rem; +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot-line a, +.unity-prompt-bar-audio .unity-paf-voice-footer-link { + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line a { + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line a:hover { + color: inherit; +} + +.unity-prompt-bar-audio .unity-paf-terms-banner-line a:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + border-radius: 2px; +} + +.unity-prompt-bar-audio .unity-paf-voice-subfoot-line a:hover { + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-footer { + margin: 4px 0 0; + text-align: center; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 12px; + line-height: 16px; + color: #fff; +} + +.unity-prompt-bar-audio .unity-paf-voice-footer-link:hover { + color: #fff; +} + +.unity-prompt-bar-audio .unity-slf-sprite { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + pointer-events: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-field { + border: none; + width: 100%; + box-sizing: border-box; + font-family: inherit; + outline: none; + background: transparent; + resize: none; + margin: 0; + scrollbar-width: thin; + scrollbar-color: #888 transparent; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-field::placeholder { + color: #7d7d7d; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .models-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .verbs-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .action-container { + position: relative; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb { + display: flex; + align-items: center; + gap: 7px; + justify-content: flex-start; + padding: 7px 12px; + cursor: pointer; + border: none; + font-size: 14px; + font-family: inherit; + font-weight: 400; + width: 100%; + min-width: unset; + background: #2c2c2c; + border-radius: 8px; + color: #fff; + height: 32px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model { + padding-left: 4px; + padding-right: 11px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .model-name { + flex: 0 1 auto; + min-width: min-content; + overflow: visible; + text-overflow: clip; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model img, +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link img { + width: 20px; + height: 20px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon { + font-size: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon svg, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon svg { + width: 12px; + height: 12px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-model .menu-icon svg, +.unity-prompt-bar-audio.unity-enabled .interactive-area .selected-verb .menu-icon svg { + filter: invert(1); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link .selected-icon { + font-size: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link .selected-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link .selected-icon svg { + width: 12px; + height: 12px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container .menu-icon { + position: relative; + top: 1px; + flex-shrink: 0; + transition: transform 0.15s ease-in; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container.show-menu .menu-icon, +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container.show-menu .menu-icon { + transform: rotate(-180deg); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container .verb-list .verb-link:not(.model-link) img { + filter: invert(1); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .models-container .model-name { + color: #fff; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .models-container .verb-list, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .verbs-container .verb-list { + background: #000; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list { + padding: 18px; + list-style: none; + box-shadow: 0 0 10px #0000001c; + border-radius: 10px; + color: #fff; + margin: 0; + animation: none; + position: absolute; + top: 0; + left: 0; + z-index: 5; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container.show-menu .verb-list, +.unity-prompt-bar-audio.unity-enabled .interactive-area .verbs-container.show-menu .verb-list { + display: block; + animation: none; + transform: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container .verb-list { + padding: 10px 12px; + min-height: 0; + box-sizing: border-box; + min-width: 100%; + width: max-content; + max-width: 100vw; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item { + margin: 0; + padding: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link { + color: inherit; + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + padding-inline-start: 25px; + text-transform: capitalize; + text-decoration: none; + text-align: start; + position: relative; + opacity: 1; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link.model-link { + padding: 8px 12px 8px 30px; + gap: 10px; + line-height: 1.25; + font-size: 14px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link.model-link img { + object-fit: contain; + flex-shrink: 0; + align-self: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-link.model-link .model-name { + align-self: center; + line-height: 20px; + white-space: nowrap; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item .selected-icon { + display: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item.selected .verb-link .selected-icon { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 8px; + top: 0; + bottom: 0; + margin-block: auto; + width: 14px; + height: 14px; + transform: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item.selected .verb-link .selected-icon svg { + display: block; +} + +[dir="rtl"] .unity-prompt-bar-audio.unity-enabled .interactive-area .verb-list .verb-item.selected .verb-link .selected-icon { + left: auto; + right: 8px; +} + +.unity-prompt-bar-audio .unity-slf-copy-label { + color: #d1d1d1; + font-family: "Adobe Clean", adobe-clean, "Adobe Clean Serif", sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; + letter-spacing: 0; +} + +.unity-prompt-bar-audio .unity-slf-prompt-label { + display: block; + margin-bottom: 8px; +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area.dark .unity-slf-prompt-label { + margin-bottom: 0; +} + +.unity-prompt-bar-audio .inp-field { + min-height: 100px; + resize: vertical; +} + +.unity-prompt-bar-audio .unity-slf-gen-btn { + white-space: nowrap; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap { + display: flex; + width: fit-content; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn { + text-decoration: none; + display: flex; + align-items: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn:focus { + outline: 2px solid #005fcc; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn .btn-ico { + display: flex; + align-content: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn .btn-ico img { + width: 22px; + height: 22px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .unity-act-btn .btn-txt { + display: flex; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .gen-btn { + flex-shrink: 0; + border-radius: 25px; + background: linear-gradient(90deg, #d73220 0%, #d92361 33%, #7155fa 100%); + border: none; + padding: 10px 20px 10px 18px; + gap: 8px; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .act-wrap .gen-btn .btn-txt { + color: var(--color-white); + font-size: 16px; + font-weight: 700; + line-height: normal; +} + +.unity-prompt-bar-audio.unity-enabled { + margin-inline: auto; + padding-bottom: 76px; +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area { + display: flex; + box-sizing: border-box; + padding: 14px; + border-radius: 20px; + gap: 8px; + isolation: isolate; + overflow-x: clip; +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area.dark { + background: rgb(24 24 24 / 48%); + backdrop-filter: blur(32px) saturate(165%); + -webkit-backdrop-filter: blur(32px) saturate(165%); + border: 1px solid rgb(255 255 255 / 14%); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 12%), + 0 16px 48px rgb(0 0 0 / 28%); +} + +.unity-prompt-bar-audio.unity-enabled > .interactive-area.light { + background: rgb(255 255 255 / 52%); + backdrop-filter: blur(32px) saturate(165%); + -webkit-backdrop-filter: blur(32px) saturate(165%); + border: 1px solid rgb(0 0 0 / 8%); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 85%), + 0 16px 48px rgb(0 0 0 / 10%); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area.dark .unity-slf-left { + background: rgb(255 255 255 / 5%); +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area.light .unity-slf-left { + background: rgb(0 0 0 / 4%); +} + +@media (prefers-reduced-transparency: reduce) { + .unity-prompt-bar-audio.unity-enabled > .interactive-area.dark { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: #222; + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: none; + } + + .unity-prompt-bar-audio.unity-enabled > .interactive-area.light { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: #eaeaea; + border: 1px solid rgb(0 0 0 / 10%); + box-shadow: none; + } + + .unity-prompt-bar-audio.unity-enabled .interactive-area.dark .unity-slf-left { + background: #1b1b1b; + } + + .unity-prompt-bar-audio.unity-enabled .interactive-area.light .unity-slf-left { + background: #fff; + } +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .autocomplete { + background: transparent !important; + border: none !important; + border-radius: 0 !important; + width: 100% !important; + max-width: 100%; + margin: 0 !important; + box-sizing: border-box; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .autocomplete::after { + content: none !important; + display: none !important; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap:not(.sticky) .ex-unity-widget { + padding-left: 0; + padding-right: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap { + display: grid; + grid-template-columns: 1fr auto; + column-gap: 8px; + align-items: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .unity-slf-copy-label, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > label { + grid-column: 1 / -1; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .inp-field { + grid-column: 1 / -1; + color: #f8f8f8; + padding: 10px 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container { + grid-column: 1; + margin-top: 32px; + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-start; + min-width: 0; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .act-wrap { + grid-column: 2; + margin-top: 24px; + display: flex; + justify-content: flex-end; + align-items: center; + align-self: center; + min-width: max-content; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container:empty { + display: none; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container:empty + .act-wrap { + grid-column: 1 / -1; + justify-self: end; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .models-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .verbs-container { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .models-container { + min-width: 0; + max-width: 100%; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .models-container { + display: flex; + width: 100%; + max-width: 100%; + position: relative; +} + +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .models-container, +.unity-prompt-bar-audio.unity-enabled .interactive-area .ex-unity-wrap.verb-options .verbs-container { + width: fit-content; +} + +@media screen and (max-width: 1199px) { + .unity-prompt-bar-audio.unity-enabled { + width: var(--grid-container-width); + } + + .unity-prompt-bar-audio .unity-paf-voice-row { + gap: 10px; + } + + .unity-prompt-bar-audio .unity-paf-voice-tile { + --unity-paf-voice-tile-width: 201px; + } +} + +@media screen and (min-width: 1200px) { + .unity-prompt-bar-audio.unity-enabled { + max-width: 1000px; + } + + .hero-marquee:has(+ .unity-prompt-bar-audio) > .foreground { + max-width: 1000px; + min-width: unset; + } + + .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .main-copy { + align-items: center; + } + + .hero-marquee:has(+ .unity-prompt-bar-audio) .foreground .main-copy p { + max-width: 800px; + } +} diff --git a/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js new file mode 100644 index 00000000..0f09c7ab --- /dev/null +++ b/unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js @@ -0,0 +1,1119 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ + +import { createTag, getConfig, getUnityPromptConfigsBaseUrl } from '../../../scripts/utils.js'; + +let promptWithStyleEvents = null; + +function buildVoiceModelIndex(voices) { + const indexMap = new Map(); + voices.forEach((voice) => { + const modelId = `${voice?.modelId ?? ''}`.trim().toLowerCase(); + if (!indexMap.has(modelId)) indexMap.set(modelId, []); + indexMap.get(modelId).push(voice); + }); + return indexMap; +} + +function filterVoicesByModelId(voices, voiceModelIndex, selectedModelId) { + const id = (selectedModelId || '').trim().toLowerCase(); + if (!id) return voices; + const shared = voiceModelIndex?.get('') || []; + const specific = voiceModelIndex?.get(id) || []; + return [...shared, ...specific]; +} + +async function loadVoicesFromCurrentPageJson(sourceUrl) { + const finalUrl = sourceUrl?.trim(); + if (!finalUrl) return []; + const res = await fetch(finalUrl); + if (!res.ok) { + throw new Error(`Current page config fetch failed: ${res.status}`); + } + const json = await res.json(); + const data = json?.content?.data; + let rows = []; + if (Array.isArray(data)) { + rows = data; + } else if (data && typeof data === 'object' && Array.isArray(data.voices)) { + rows = data.voices; + } + if (!rows.length) return []; + return rows + .filter((row) => row && typeof row === 'object') + .map((row) => ({ + name: `${row.Name ?? ''}`.trim(), + description: `${row.Description ?? ''}`.trim(), + url: `${row.url ?? ''}`.trim(), + voiceId: `${row.VoiceId ?? ''}`.trim(), + modelId: `${row.ModelId ?? ''}`.trim(), + })); +} + +function resolveCurrentPageVariationFileUrl(root) { + const el = root?.querySelector?.('[class*="icon-operation-"]'); + const token = el && [...el.classList].find((c) => c.startsWith('icon-operation-')); + const rawSuffix = token ? token.slice('icon-operation-'.length) : ''; + const normalized = rawSuffix.trim().replace(/\s+/g, '-'); + const base = normalized.replace(/[^a-zA-Z0-9._-]/g, ''); + const fileBase = base ? base.toLowerCase() : null; + if (!fileBase) return null; + const baseUrl = getUnityPromptConfigsBaseUrl(); + const { locale } = getConfig(); + return locale.prefix && locale.prefix !== '/' + ? `${baseUrl}${locale.prefix}/unity/configs/prompt/${fileBase}.json` + : `${baseUrl}/unity/configs/prompt/${fileBase}.json`; +} + +class UnityWidget { + constructor(target, el, workflowCfg, spriteCon) { + this.el = el; + this.target = target; + this.workflowCfg = workflowCfg; + this.widget = null; + this.actionMap = {}; + this.spriteCon = spriteCon; + this.prompts = null; + this.models = null; + this.selectedVerbType = ''; + this.selectedVerbText = ''; + this.selectedModelModule = ''; + this.selectedModelId = ''; + this.selectedModelText = ''; + this.selectedModelVersion = ''; + this.selectedModelName = ''; + this.promptItems = []; + this.genBtn = null; + this.hasPromptSuggestions = false; + this.hasModelOptions = false; + this.voices = null; + this.voiceConfigAll = null; + this.voiceModelIndex = null; + this.lanaOptions = { sampleRate: 100, tags: 'Unity-FF' }; + this.sound = { audio: null, currentTile: null, currentUrl: '' }; + this.durationCache = new Map(); + } + + verbDropdown() { + const verb = this.el.querySelector('[class*="icon-verb"]'); + const selectedVerb = verb?.nextElementSibling; + this.selectedVerbType = verb?.className.split('-')[2]; + this.selectedVerbText = selectedVerb?.textContent.trim() ?? ''; + this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType); + } + + closeVerbOrModelMenu(selectedElement) { + const menuContainer = selectedElement?.parentElement; + if (!menuContainer) return; + menuContainer.classList.remove('show-menu'); + selectedElement.setAttribute('aria-expanded', 'false'); + const list = selectedElement.nextElementSibling; + if (list?.classList?.contains('verb-list')) { + list.setAttribute('style', 'display: none;'); + } + } + + showVerbMenu(selectedElement) { + const menuContainer = selectedElement.parentElement; + document.querySelectorAll('.models-container').forEach((container) => { + if (container !== menuContainer) { + const sm = container.querySelector('.selected-model'); + if (sm) this.closeVerbOrModelMenu(sm); + } + }); + menuContainer.classList.toggle('show-menu'); + const isOpen = menuContainer.classList.contains('show-menu'); + selectedElement.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + const siblingList = selectedElement.nextElementSibling; + if (siblingList?.classList?.contains('verb-list')) { + if (isOpen) { + siblingList.removeAttribute('style'); + } else { + siblingList.setAttribute('style', 'display: none;'); + } + } + } + + showVerbOrModelMenuAndTrackOpen(selectedElement, adobeEventName) { + const menuContainer = selectedElement.parentElement; + const wasOpen = menuContainer.classList.contains('show-menu'); + this.hidePromptDropdown(selectedElement); + this.showVerbMenu(selectedElement); + if (!wasOpen) { + this.widgetWrap.dispatchEvent(new CustomEvent('firefly-analytics', { + detail: { + adobeEventName, + splunkData: { action: 'open' }, + }, + })); + } + } + + hidePromptDropdown(exceptElement = null) { + const dropdown = this.widget.querySelector('.prompt-dropdown-container'); + if (dropdown && !dropdown.classList.contains('hidden')) { + dropdown.classList.add('hidden'); + dropdown.setAttribute('inert', ''); + dropdown.setAttribute('aria-hidden', 'true'); + } + if (this.selectedVerbType === 'sound') { + this.resetAllSoundVariations?.(dropdown); + } + const modelDropdown = this.widget.querySelector('.models-container'); + const modelButton = modelDropdown?.querySelector('.selected-model'); + if (modelDropdown && modelDropdown.classList.contains('show-menu') && modelButton && modelButton !== exceptElement) { + this.closeVerbOrModelMenu(modelButton); + } + } + + updateAnalytics(verb) { + if (this.promptItems && this.promptItems.length > 0) { + this.promptItems.forEach((item) => { + const ariaLabel = item.getAttribute('aria-label') || ''; + item.setAttribute('daa-ll', `${ariaLabel.slice(0, 20)}--${verb}--Prompt suggestion`); + }); + } + } + + clearSelectedModelState() { + this.selectedModelId = ''; + this.selectedModelName = ''; + this.selectedModelVersion = ''; + this.selectedModelModule = ''; + this.selectedModelText = ''; + this.widgetWrap?.removeAttribute('data-selected-model-name'); + } + + handleModelLinkClick(link, listContainer, selectedElement, menuIcon) { + return (e) => { + e.preventDefault(); + e.stopPropagation(); + const verbLinkTexts = []; + listContainer.querySelectorAll('.verb-link').forEach((listLink) => { + listLink.parentElement.classList.remove('selected'); + listLink.setAttribute('aria-selected', 'false'); + const text = listLink.textContent.trim(); + if (text) verbLinkTexts.push(text); + }); + verbLinkTexts.sort((a, b) => b.length - a.length); + this.closeVerbOrModelMenu(selectedElement); + link.parentElement.classList.add('selected'); + link.setAttribute('aria-selected', 'true'); + this.selectedModelId = link.getAttribute('data-model-id'); + this.selectedModelName = link.textContent.trim(); + this.selectedModelVersion = link.getAttribute('data-model-version'); + this.selectedModelModule = link.getAttribute('data-model-module'); + this.selectedModelText = link.textContent.trim(); + const copiedNodes = link.cloneNode(true).childNodes; + copiedNodes[0].remove(); + selectedElement.replaceChildren(...copiedNodes, menuIcon); + selectedElement.dataset.selectedModelId = this.selectedModelId; + selectedElement.dataset.selectedModelVersion = this.selectedModelVersion; + selectedElement.focus(); + const verbsWithoutPromptSuggestions = this.workflowCfg.targetCfg?.verbsWithoutPromptSuggestions ?? []; + if (verbsWithoutPromptSuggestions.includes(this.selectedVerbType)) { + this.widgetWrap.dispatchEvent(new CustomEvent('firefly-reinit-action-listeners')); + } + if (link.getAttribute('data-model-module') !== this.selectedVerbType) { + const oldModelContainer = this.widget.querySelector('.models-container'); + const modelDropdown = this.modelDropdown(); + if (oldModelContainer) { + if (modelDropdown.length > 1) { + const newModelContainer = createTag('div', { class: 'models-container', 'aria-label': 'Model options' }); + newModelContainer.append(...modelDropdown); + oldModelContainer.replaceWith(newModelContainer); + } else { + oldModelContainer.remove(); + this.clearSelectedModelState(); + } + } else if (modelDropdown.length > 1) { + const actionContainer = this.widget.querySelector('.action-container'); + if (actionContainer) { + const newModelContainer = createTag('div', { class: 'models-container', 'aria-label': 'Prompt options' }); + newModelContainer.append(...modelDropdown); + actionContainer.append(newModelContainer); + } + } else this.clearSelectedModelState(); + } + this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType); + if (this.selectedModelId) { + this.widgetWrap.setAttribute('data-selected-model-id', this.selectedModelId); + this.widgetWrap.setAttribute('data-selected-model-name', this.selectedModelName || ''); + } else { + this.widgetWrap.removeAttribute('data-selected-model-id'); + this.widgetWrap.removeAttribute('data-selected-model-name'); + } + if (this.selectedModelVersion) this.widgetWrap.setAttribute('data-selected-model-version', this.selectedModelVersion); + else this.widgetWrap.removeAttribute('data-selected-model-version'); + this.updateAnalytics(this.selectedVerbType); + if (typeof this.refreshVoiceTilesForModel === 'function') { + this.refreshVoiceTilesForModel(); + } + if (this.genBtn) { + const img = this.genBtn.querySelector('img[src*=".svg"]'); + this.genBtn.setAttribute( + 'aria-label', + (this.genBtn.getAttribute('aria-label') || '').replace( + new RegExp(`\\b(${verbLinkTexts.join('|')})\\b`), + this.selectedVerbText, + ), + ); + if (img) img.setAttribute('alt', `${this.genBtn.getAttribute('aria-label') || ''}`); + } + }; + } + + createDropdownItems(items, listContainer, selectedElement, menuIcon) { + const fragment = document.createDocumentFragment(); + items.forEach((item, idx) => { + const { name, icon, module, id, version } = item; + const listItem = createTag('li', { + class: 'verb-item', + role: 'presentation', + }); + const selectedIcon = createTag('span', { class: 'selected-icon' }, ''); + const nameContainer = createTag('span', { class: 'model-name' }, name.trim()); + const link = createTag('a', { + href: '#', + class: 'verb-link model-link', + 'data-model-module': module, + 'data-model-id': id, + 'data-model-version': version, + 'aria-selected': 'false', + role: 'option', + }, `${nameContainer.outerHTML}`); + if (idx === 0) { + listItem.classList.add('selected'); + link.setAttribute('aria-selected', 'true'); + } + link.prepend(selectedIcon); + listItem.append(link); + fragment.append(listItem); + }); + listContainer.append(fragment); + listContainer.addEventListener('click', (e) => { + const link = e.target.closest('.verb-link'); + if (!link) return; + this.handleModelLinkClick(link, listContainer, selectedElement, menuIcon)(e); + }); + listContainer.addEventListener('keydown', (e) => { + if (e.key !== 'Tab') return; + const menuContainer = selectedElement.parentElement; + if (!menuContainer?.classList.contains('show-menu')) return; + const links = listContainer.querySelectorAll('.verb-link'); + if (!links.length) return; + const active = document.activeElement; + const idx = [...links].findIndex((a) => a === active || a.contains(active)); + if (idx < 0) return; + const atStart = idx === 0; + const atEnd = idx === links.length - 1; + if ((e.shiftKey && atStart) || (!e.shiftKey && atEnd)) { + this.closeVerbOrModelMenu(selectedElement); + } + }); + } + + modelDropdown() { + if (!this.hasModelOptions) return []; + const models = Array.isArray(this.models) + ? this.models.filter((obj) => obj.module === this.selectedVerbType) + : []; + if (models.length === 0) return []; + const selectedModelType = models[0].id; + const selectedModelVersion = models[0].version; + const selectedModelModule = models[0].module; + const selectedModelName = models[0].name.trim(); + const nameContainer = createTag('span', { class: 'model-name' }, models[0].name.trim()); + const selectedElement = createTag('button', { + class: 'selected-model', + 'aria-expanded': 'false', + 'aria-controls': 'model-menu', + 'aria-label': 'model type', + 'aria-haspopup': 'listbox', + role: 'combobox', + 'aria-labelledby': 'listbox-label', + 'data-selected-model-id': selectedModelType, + 'data-selected-model-version': selectedModelVersion, + 'data-selected-model-module': selectedModelModule, + }, `${nameContainer.outerHTML}`); + this.selectedModelModule = selectedModelModule; + this.selectedModelId = selectedModelType; + this.selectedModelVersion = selectedModelVersion; + this.selectedModelName = selectedModelName; + this.widgetWrap.setAttribute('data-selected-model-id', this.selectedModelId); + this.widgetWrap.setAttribute('data-selected-model-version', this.selectedModelVersion); + this.widgetWrap.setAttribute('data-selected-model-name', this.selectedModelName); + this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType); + this.selectedModelText = models[0].name.trim(); + const menuIcon = createTag('span', { class: 'menu-icon' }, ''); + const listItems = createTag('ul', { class: 'verb-list', id: 'model-menu', role: 'listbox', 'aria-labelledby': 'listbox-label' }); + listItems.setAttribute('style', 'display: none;'); + selectedElement.append(menuIcon); + const handleDocumentClick = (e) => { + const menuContainer = selectedElement.parentElement; + if (!menuContainer.contains(e.target)) { + document.removeEventListener('click', handleDocumentClick); + this.closeVerbOrModelMenu(selectedElement); + } + }; + selectedElement.addEventListener('click', (e) => { + e.stopPropagation(); + this.showVerbOrModelMenuAndTrackOpen(selectedElement, promptWithStyleEvents.MODEL_SELECT_DROPDOWN); + document.addEventListener('click', handleDocumentClick); + }, true); + selectedElement.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + this.showVerbOrModelMenuAndTrackOpen(selectedElement, promptWithStyleEvents.MODEL_SELECT_DROPDOWN); + } + if (e.key === 'Escape') { + this.closeVerbOrModelMenu(selectedElement); + selectedElement.focus(); + } + }); + this.createDropdownItems(models, listItems, selectedElement, menuIcon); + return [selectedElement, listItems]; + } + + createActBtn(cfg, cls) { + if (!cfg) return null; + const txt = cfg.innerText?.trim(); + const img = cfg.querySelector('img[src*=".svg"]'); + if (img) img.setAttribute('alt', `${txt?.split('\n')[0]} ${this.selectedVerbText}`); + const btn = createTag('a', { href: '#', class: `unity-act-btn ${cls}`, 'daa-ll': promptWithStyleEvents.GENERATE_CTA, 'aria-label': `${txt?.split('\n')[0]} ${this.selectedVerbText}` }); + if (img) btn.append(createTag('div', { class: 'btn-ico' }, img)); + if (txt) btn.append(createTag('div', { class: 'btn-txt' }, txt.split('\n')[0])); + this.genBtn = btn; + return btn; + } + + async loadModels() { + const modelFile = `${getUnityPromptConfigsBaseUrl()}/unity/configs/prompt/model-picker.json`; + const results = await fetch(modelFile); + if (!results.ok) { + throw new Error('Failed to fetch models.'); + } + const modelJson = await results.json(); + this.models = modelJson?.content?.data; + } + + async getModel() { + if (!this.hasModelOptions) return []; + try { + if (!this.models || Object.keys(this.models).length === 0) await this.loadModels(); + return this.models; + } catch (e) { + window.lana?.log(`Message: Error loading models, Error: ${e}`, this.lanaOptions); + return []; + } + } +} + +const RING_C = 2 * Math.PI * 20; +const RING_STROKE_ATTR = `${(2.78751 * 48) / 33}`; +const PAF_PP_PLAY_SVG = ''; +const PAF_PP_PAUSE_SVG = ''; +const PAF_PROGRESS_SVG = ``; +const PAF_PLAYER_LOADING_SVG = ``; +const voiceTileState = new WeakMap(); + +function setVoiceTilePlayerBuffering(tile, isBuffering) { + const p = voiceTileState.get(tile); + if (!p?.player || !p.bufferLayer || !p.progressSvg || !p.center) return; + const on = Boolean(isBuffering); + if (on === p.bufferingUi) return; + p.bufferingUi = on; + if (on) { + p.player.classList.add('unity-paf-voice-player--buffering'); + p.bufferLayer.setAttribute('aria-busy', 'true'); + p.player.replaceChildren(p.bufferLayer); + } else { + p.player.classList.remove('unity-paf-voice-player--buffering'); + p.bufferLayer.removeAttribute('aria-busy'); + p.player.replaceChildren(p.progressSvg, p.center); + } +} + +function setVoiceTileCenterIcon(tile, iconSvg) { + const p = voiceTileState.get(tile); + if (!p?.center) return; + setVoiceTilePlayerBuffering(tile, false); + p.center.innerHTML = iconSvg; +} + +function primeVoiceAudioForPlayback(tile) { + const p = voiceTileState.get(tile); + if (!p || p.audio.src) return; + setVoiceTilePlayerBuffering(tile, true); + p.audio.preload = 'auto'; + p.audio.src = p.url; +} + +function findPlaceholderIconLi(root, iconClass) { + const icon = root.querySelector(`.${iconClass}`) + || root.querySelector(`[class*="${iconClass}"]`); + return icon?.closest('li') ?? null; +} + +function placeholderRowText(root, iconClass) { + const li = findPlaceholderIconLi(root, iconClass); + if (!li) return ''; + return (li.innerText || '').replace(/\s+/g, ' ').trim(); +} + +function placeholderRowHtmlAfterIcon(root, iconClass) { + const li = findPlaceholderIconLi(root, iconClass); + if (!li) return ''; + const clone = li.cloneNode(true); + const rm = clone.querySelector(`.${iconClass}`) || clone.querySelector(`[class*="${iconClass}"]`); + if (rm) rm.remove(); + return (clone.innerHTML || '').replace(/^\s+/, '').trim(); +} + +function findFooterLinkInRoot(root) { + const anchors = Array.from(root.querySelectorAll('a[href^="https://"]')); + const a = anchors.find((el) => { + const href = el.getAttribute('href')?.trim() ?? ''; + if (!href) return false; + try { + if (/\.json$/i.test(new URL(href, window.location.href).pathname)) return false; + } catch { + return false; + } + return true; + }); + if (!a) return null; + const href = a.getAttribute('href')?.trim() ?? ''; + return { href, text: a.textContent?.trim() || href }; +} + +function dispatchAudioPlaybackFailed(widgetWrap) { + try { + widgetWrap?.dispatchEvent(new CustomEvent('firefly-audio-error', { detail: { error: 'audio-playback-failed' } })); + } catch (e) { + window.lana?.log(`Message: Error dispatching audio playback failed event, Error: ${e}`, this.lanaOptions); + } +} + +export function parsePromptBarAudioAuthoring(root) { + return { + footerLink: findFooterLinkInRoot(root), + sectionHeading: placeholderRowText(root, 'icon-placeholder-voice') || 'Choose a voice', + currentPageSourceUrl: resolveCurrentPageVariationFileUrl(root), + defaultPrompt: placeholderRowText(root, 'placeholder-prompt-default') || '', + exploreHtml: placeholderRowHtmlAfterIcon(root, 'placeholder-explore'), + termsHtml: placeholderRowHtmlAfterIcon(root, 'placeholder-terms'), + }; +} + +function buildVoiceTile(voice, index, row, widgetInstance) { + const { name, description, url, voiceId } = voice; + const tile = createTag('div', { + class: `unity-paf-voice-tile${index === 0 ? ' selected' : ''}`, + role: 'listitem', + tabindex: '0', + 'aria-pressed': 'false', + 'data-voice-index': String(index), + 'data-voice-name': name, + }); + if (voiceId) tile.setAttribute('data-voice-id', voiceId); + if (index === 0) tile.setAttribute('aria-current', 'true'); + + const textCol = createTag('div', { class: 'unity-paf-voice-tile-text' }); + textCol.append( + createTag('span', { class: 'unity-paf-voice-name' }, name), + createTag('span', { class: 'unity-paf-voice-desc' }, description), + ); + + const player = createTag('div', { class: 'unity-paf-voice-player' }); + player.insertAdjacentHTML('beforeend', PAF_PROGRESS_SVG); + const progressSvg = player.querySelector('.unity-paf-progress-svg'); + const ringFg = progressSvg?.querySelector('.unity-paf-ring-fg'); + if (!progressSvg || !ringFg) return tile; + ringFg.style.strokeDasharray = String(RING_C); + ringFg.style.strokeDashoffset = String(RING_C); + const center = createTag('div', { class: 'unity-paf-pp-center' }); + center.innerHTML = PAF_PP_PLAY_SVG; + const bufferLayer = createTag('div', { class: 'unity-paf-voice-player-loading' }); + bufferLayer.innerHTML = PAF_PLAYER_LOADING_SVG; + const audioObj = new Audio(); + audioObj.preload = 'none'; + voiceTileState.set(tile, { + audio: audioObj, + ringFg, + player, + bufferLayer, + progressSvg, + center, + playing: false, + url, + bufferingUi: false, + }); + player.append(progressSvg, center); + tile.append(textCol, player); + row.append(tile); + const setRingProgress = (t) => { + const a = audioObj; + if (!Number.isFinite(a.duration) || a.duration <= 0) return; + const p = t / a.duration; + ringFg.style.strokeDashoffset = String(RING_C * (1 - p)); + }; + const showPlayIcon = () => setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + const showPauseIcon = () => setVoiceTileCenterIcon(tile, PAF_PP_PAUSE_SVG); + let rafId = null; + const tick = () => { + if (audioObj.paused && !audioObj.ended) { + rafId = null; + return; + } + setRingProgress(audioObj.currentTime); + rafId = requestAnimationFrame(tick); + }; + const startRaf = () => { + if (rafId) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(tick); + }; + const stopRaf = () => { + if (rafId) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + audioObj.addEventListener('loadedmetadata', () => { + const dur = Number.isFinite(audioObj.duration) && audioObj.duration > 0 ? audioObj.duration : 0; + if (dur > 0) widgetInstance.durationCache.set(url, dur); + setRingProgress(0); + }); + audioObj.addEventListener('play', () => { + voiceTileState.get(tile).playing = true; + showPauseIcon(); + tile.setAttribute('aria-pressed', 'true'); + startRaf(); + }); + audioObj.addEventListener('pause', () => { + const atEnd = audioObj.ended || (Number.isFinite(audioObj.duration) && audioObj.currentTime >= audioObj.duration - 0.25); + voiceTileState.get(tile).playing = !atEnd && audioObj.currentTime > 0; + showPlayIcon(); + if (atEnd) { + setRingProgress(0); + ringFg.style.strokeDashoffset = String(RING_C); + } else { + setRingProgress(audioObj.currentTime); + } + tile.setAttribute('aria-pressed', 'false'); + stopRaf(); + }); + audioObj.addEventListener('ended', () => { + voiceTileState.get(tile).playing = false; + showPlayIcon(); + setRingProgress(0); + ringFg.style.strokeDashoffset = String(RING_C); + tile.setAttribute('aria-pressed', 'false'); + try { audioObj.currentTime = 0; } catch (e) { /* noop */ } + stopRaf(); + }); + audioObj.addEventListener('error', () => { + setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + dispatchAudioPlaybackFailed(widgetInstance.widgetWrap); + }); + audioObj.addEventListener('waiting', () => { + if (!audioObj.paused) setVoiceTilePlayerBuffering(tile, true); + }); + audioObj.addEventListener('playing', () => { + if (!audioObj.paused) showPauseIcon(); + }); + return tile; +} + +function attachVoiceInteractivity(tiles, widgetInstance, inpField, voices) { + const wrap = widgetInstance.widgetWrap; + let selectedIdx = 0; + const authoring = (widgetInstance.defaultPromptFromAuthoring ?? '').trim(); + + function setSelectedVisual(idx) { + selectedIdx = idx; + if (idx < 0) { + wrap.removeAttribute('data-selected-voice-index'); + wrap.removeAttribute('data-selected-voice-name'); + wrap.removeAttribute('data-selected-voice-id'); + tiles.forEach((t) => { + t.classList.remove('selected'); + t.removeAttribute('aria-current'); + }); + return; + } + wrap.setAttribute('data-selected-voice-index', String(idx)); + wrap.setAttribute('data-selected-voice-name', voices[idx]?.name ?? ''); + const voiceId = voices[idx]?.voiceId; + if (voiceId) wrap.setAttribute('data-selected-voice-id', voiceId); + else wrap.removeAttribute('data-selected-voice-id'); + tiles.forEach((t, i) => { + t.classList.toggle('selected', i === idx); + if (i === idx) t.setAttribute('aria-current', 'true'); + else t.removeAttribute('aria-current'); + }); + } + + function resetTileIdle(tile) { + const p = voiceTileState.get(tile); + if (!p) return; + try { p.audio.pause(); } catch { /* ignore */ } + try { p.audio.currentTime = 0; } catch { /* ignore */ } + p.playing = false; + p.ringFg.style.strokeDashoffset = String(RING_C); + setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + tile.setAttribute('aria-pressed', 'false'); + } + + function syncPromptIfStuckToDefaults() { + const { value } = inpField; + if (value === authoring || value.trim() === '') { + inpField.value = authoring; + } + } + + function toggleTile(idx) { + const tile = tiles[idx]; + const p = tile && voiceTileState.get(tile); + if (!p) return; + const { audio } = p; + const isPlaying = !audio.paused && !audio.ended; + if (isPlaying) { + audio.pause(); + return; + } + primeVoiceAudioForPlayback(tile); + audio.play().catch(() => { + setVoiceTileCenterIcon(tile, PAF_PP_PLAY_SVG); + dispatchAudioPlaybackFailed(wrap); + }); + } + + function onTileActivate(idx) { + if (idx !== selectedIdx) { + syncPromptIfStuckToDefaults(); + setSelectedVisual(idx); + tiles.forEach((t, i) => { if (i !== idx) resetTileIdle(t); }); + const nextTile = tiles[idx]; + primeVoiceAudioForPlayback(nextTile); + voiceTileState.get(nextTile).audio.play().catch(() => { + setVoiceTileCenterIcon(nextTile, PAF_PP_PLAY_SVG); + dispatchAudioPlaybackFailed(wrap); + }); + return; + } + toggleTile(idx); + } + + tiles.forEach((tile, idx) => { + tile.addEventListener('click', (ev) => { + if (ev.target?.closest && ev.target.closest('a[href]')) return; + ev.preventDefault(); + onTileActivate(idx); + }); + tile.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onTileActivate(idx); + } + }); + }); + setSelectedVisual(0); + return () => { tiles.forEach(resetTileIdle); }; +} + +function createPromptAudioShellBase(widgetInstance, el) { + const widgetWrap = createTag('div', { class: 'ex-unity-wrap verb-options' }); + const [widget, unitySprite] = ['ex-unity-widget', 'unity-sprite-container'] + .map((c) => createTag('div', { class: c })); + widgetInstance.widgetWrap = widgetWrap; + widgetInstance.widget = widget; + unitySprite.innerHTML = widgetInstance.spriteCon; + unitySprite.classList.add('unity-slf-sprite'); + widgetWrap.append(unitySprite); + const phStub = createTag('div', { hidden: true, 'aria-hidden': 'true' }); + phStub.innerHTML = ''; + el.append(phStub); + return { widgetWrap, widget }; +} + +function createPromptAudioInputField(widgetInstance, defaultPrompt, pws) { + const inpField = createTag('textarea', { + id: 'promptInput', + class: 'inp-field', + 'aria-autocomplete': 'list', + 'aria-haspopup': 'listbox', + rows: '4', + }); + inpField.value = defaultPrompt; + let promptEngagedTracked = false; + inpField.addEventListener('pointerdown', (e) => { + if (e.button !== 0 || e.target !== inpField) return; + widgetInstance.hidePromptDropdown(); + if (!promptEngagedTracked && pws?.ENTER_PROMPT) { + promptEngagedTracked = true; + const det = { adobeEventName: pws.ENTER_PROMPT, splunkData: { action: 'enter-prompt' } }; + widgetInstance.widgetWrap?.dispatchEvent(new CustomEvent('firefly-analytics', { detail: det })); + } + }); + inpField.addEventListener('blur', () => { promptEngagedTracked = false; }); + let emptyPromptRestoreTimerId = null; + const clearEmptyPromptRestoreTimer = () => { + if (emptyPromptRestoreTimerId != null) { + clearTimeout(emptyPromptRestoreTimerId); + emptyPromptRestoreTimerId = null; + } + }; + widgetInstance.clearEmptyPromptRestoreTimer = clearEmptyPromptRestoreTimer; + inpField.addEventListener('input', () => { + const trimmed = (inpField.value || '').trim(); + if (trimmed !== '') { + clearEmptyPromptRestoreTimer(); + return; + } + clearEmptyPromptRestoreTimer(); + emptyPromptRestoreTimerId = window.setTimeout(() => { + emptyPromptRestoreTimerId = null; + if (!inpField.isConnected) return; + if ((inpField.value || '').trim() !== '') return; + inpField.value = (widgetInstance.defaultPromptFromAuthoring ?? '').trim(); + }, 10000); + }); + return inpField; +} + +function createPromptAudioActionContainer(widgetInstance, widgetWrap, modelParts) { + const actionContainer = createTag('div', { class: 'action-container' }); + if (modelParts.length > 1) { + const modelBtn = createTag('div', { class: 'models-container', 'aria-label': 'Model options' }); + modelBtn.append(...modelParts); + actionContainer.append(modelBtn); + return actionContainer; + } + widgetWrap.setAttribute('data-selected-model-id', 'adobe-firefly'); + widgetWrap.setAttribute('data-selected-model-version', 'image3'); + const fallbackName = Array.isArray(widgetInstance.models) + ? widgetInstance.models.find((m) => m.id === 'adobe-firefly' && (!m.version || m.version === 'image3'))?.name?.trim() + || widgetInstance.models.find((m) => m.id === 'adobe-firefly')?.name?.trim() + : ''; + if (fallbackName) widgetWrap.setAttribute('data-selected-model-name', fallbackName); + return actionContainer; +} + +function createPromptAudioGenerateButton(widgetInstance, el, pws) { + const generateLi = el.querySelector('.icon-generate')?.closest('li'); + let genBtn = widgetInstance.createActBtn(generateLi, 'gen-btn unity-slf-gen-btn'); + if (!genBtn) { + genBtn = createTag('a', { + href: '#', + class: 'unity-act-btn gen-btn unity-slf-gen-btn', + 'daa-ll': pws?.GENERATE_CTA ?? 'Generate', + 'aria-label': 'Generate', + }); + genBtn.append(createTag('div', { class: 'btn-txt' }, 'Generate')); + widgetInstance.genBtn = genBtn; + return genBtn; + } + if (!genBtn.querySelector('.btn-ico') && generateLi) { + const svgHref = generateLi.querySelector('a[href$=".svg"]')?.href; + if (svgHref) genBtn.prepend(createTag('div', { class: 'btn-ico' }, createTag('img', { src: svgHref, alt: 'Generate' }))); + } + return genBtn; +} + +function composePromptAudioInputLayout(widget, promptLabelText, inpField, actionContainer, genBtn) { + const inpWrap = createTag('div', { class: 'inp-wrap' }); + const promptLabel = createTag( + 'label', + { for: 'promptInput', class: 'unity-slf-copy-label unity-slf-prompt-label' }, + promptLabelText, + ); + const actWrap = createTag('div', { class: 'act-wrap' }); + actWrap.append(genBtn); + inpWrap.append(promptLabel, inpField, actionContainer, actWrap); + const comboboxContainer = createTag('div', { class: 'autocomplete' }); + comboboxContainer.append(inpWrap); + widget.append(comboboxContainer); +} + +function createPromptAudioInputShell(widgetInstance, el, defaultPrompt, analyticsMod) { + const pws = analyticsMod?.PROMPT_WITH_STYLE_EVENTS; + const { widgetWrap, widget } = createPromptAudioShellBase(widgetInstance, el); + widgetInstance.hasModelOptions = !!el.querySelector('[class*="icon-model"]'); + widgetInstance.verbDropdown(); + const modelParts = widgetInstance.modelDropdown(); + const promptLabelText = placeholderRowText(el, 'placeholder-prompt-label'); + const inpField = createPromptAudioInputField(widgetInstance, defaultPrompt, pws); + const actionContainer = createPromptAudioActionContainer(widgetInstance, widgetWrap, modelParts); + const genBtn = createPromptAudioGenerateButton(widgetInstance, el, pws); + composePromptAudioInputLayout(widget, promptLabelText, inpField, actionContainer, genBtn); + widgetWrap.append(widget); + return { widgetWrap, widget, inpField }; +} + +function appendVoiceExploreSubfoot(section, exploreHtml, footerLink) { + const ex = (exploreHtml || '').trim(); + if (ex) { + const wrap = createTag('div', { class: 'unity-paf-voice-subfoot' }); + const p = createTag('p', { class: 'unity-paf-voice-subfoot-line' }); + p.innerHTML = ex; + wrap.append(p); + section.append(wrap); + } else if (footerLink) { + const foot = createTag('p', { class: 'unity-paf-voice-footer' }); + const a = createTag('a', { href: footerLink.href, class: 'unity-paf-voice-footer-link' }); + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.textContent = footerLink.text; + foot.append(a); + section.append(foot); + } +} + +function buildTermsBannerElement(termsHtml) { + const te = (termsHtml || '').trim(); + if (!te) return null; + const outer = createTag('div', { + class: 'unity-paf-terms-banner', + role: 'note', + }); + const p = createTag('p', { class: 'unity-paf-terms-banner-line' }); + p.innerHTML = te; + outer.append(p); + return outer; +} + +function syncVoiceRowPeekClasses(row, shouldPeek) { + if (!row) return; + row.classList.toggle('unity-paf-voice-row-peek', shouldPeek); + row.classList.toggle('unity-paf-voice-row-peek-scrolled', shouldPeek && row.scrollLeft > 0); +} + +function wireVoiceRowPeekTracking(widgetInstance, row, shouldPeek) { + if (widgetInstance.detachVoiceRowPeekTracking) { + try { widgetInstance.detachVoiceRowPeekTracking(); } catch (e) { /* noop */ } + widgetInstance.detachVoiceRowPeekTracking = null; + } + if (!row) { + syncVoiceRowPeekClasses(null, false); + return; + } + const onScroll = () => { + const active = row.classList.contains('unity-paf-voice-row-peek'); + syncVoiceRowPeekClasses(row, active); + }; + row.addEventListener('scroll', onScroll, { passive: true }); + widgetInstance.detachVoiceRowPeekTracking = () => row.removeEventListener('scroll', onScroll); + syncVoiceRowPeekClasses(row, shouldPeek); +} + +function createVoiceStrip(allVoices, visibleVoices, sectionHeading, footerLink, widgetInstance, opts = {}) { + const { exploreHtml = '' } = opts; + if (!allVoices.length) return { section: null, tiles: [] }; + if (!visibleVoices.length) { + const sectionEmpty = createTag('div', { class: 'unity-paf-voice-section' }); + const headingEmpty = createTag( + 'p', + { class: 'unity-slf-copy-label unity-paf-voice-heading' }, + sectionHeading, + ); + const rowEmpty = createTag('div', { class: 'unity-paf-voice-row', role: 'list', 'aria-label': 'Voice samples' }); + sectionEmpty.append(headingEmpty, rowEmpty); + appendVoiceExploreSubfoot(sectionEmpty, exploreHtml, footerLink); + return { section: sectionEmpty, tiles: [] }; + } + const section = createTag('div', { class: 'unity-paf-voice-section' }); + const heading = createTag( + 'p', + { class: 'unity-slf-copy-label unity-paf-voice-heading' }, + sectionHeading, + ); + const row = createTag('div', { class: 'unity-paf-voice-row', role: 'list', 'aria-label': 'Voice samples' }); + syncVoiceRowPeekClasses(row, visibleVoices.length > 4); + const tiles = visibleVoices.map((v, i) => buildVoiceTile(v, i, row, widgetInstance)); + section.append(heading, row); + appendVoiceExploreSubfoot(section, exploreHtml, footerLink); + return { section, tiles }; +} + +function insertPromptBarAudioRoot(el, widgetInstance, widgetWrap, voiceSection, termsBanner) { + const controls = createTag('div', { class: 'unity-slf-controls' }); + controls.append(widgetWrap); + if (voiceSection) controls.append(voiceSection); + const left = createTag('div', { class: 'unity-slf-left' }); + left.append(controls); + const main = createTag('div', { class: 'unity-paf-main' }); + main.append(left); + const skin = el.classList.contains('light') ? 'light' : 'dark'; + const interactiveShell = createTag('div', { class: `interactive-area ${skin}` }); + const root = createTag('div', { class: 'unity-prompt-bar-audio unity-enabled' }); + interactiveShell.append(main); + root.append(interactiveShell); + if (termsBanner) root.append(termsBanner); + const holder = createTag('div', { class: 'unity-slf-config-holder unity-slf-sr-only' }); + holder.setAttribute('aria-hidden', 'true'); + while (el.firstChild) { + holder.append(el.firstChild); + } + el.append(holder); + el.classList.add('unity-prompt-bar-audio-host'); + if (el.parentNode) { + el.parentNode.insertBefore(root, el); + } else { + el.append(root); + } + widgetInstance.promptBarExtendedRoot = root; +} + +async function mountPromptBarAudioUI(widgetInstance, parsed) { + const { + voices, + footerLink, + sectionHeading, + exploreHtml = '', + termsHtml = '', + } = parsed; + const authoring = (widgetInstance.defaultPromptFromAuthoring ?? '').trim(); + const [analyticsMod] = await Promise.all([ + import('../../../scripts/analytics.js'), + widgetInstance.hasModelOptions ? widgetInstance.getModel() : Promise.resolve(), + ]); + promptWithStyleEvents = analyticsMod.PROMPT_WITH_STYLE_EVENTS; + const { el } = widgetInstance; + const { widgetWrap, inpField } = createPromptAudioInputShell(widgetInstance, el, '', analyticsMod); + const selectedModelId = (widgetInstance.selectedModelId + || widgetInstance.widgetWrap?.getAttribute('data-selected-model-id') + || '').trim(); + const visibleVoices = filterVoicesByModelId(voices, widgetInstance.voiceModelIndex, selectedModelId); + inpField.value = authoring; + const { section: voiceSection, tiles } = createVoiceStrip( + voices, + visibleVoices, + sectionHeading, + footerLink, + widgetInstance, + { exploreHtml: exploreHtml || '' }, + ); + const termsBanner = buildTermsBannerElement(termsHtml || ''); + const disconnectFirst = visibleVoices.length + ? attachVoiceInteractivity(tiles, widgetInstance, inpField, visibleVoices) + : () => {}; + widgetInstance.voicePromptInpField = inpField; + widgetInstance.teardownVoiceTiles = disconnectFirst; + widgetInstance.refreshVoiceTilesForModel = function refreshVoiceTilesForModel() { + const all = this.voiceConfigAll; + if (!all || !all.length) return; + const root = this.promptBarExtendedRoot; + const row = root?.querySelector('.unity-paf-voice-row') ?? null; + if (!row) return; + if (this.teardownVoiceTiles) { + try { this.teardownVoiceTiles(); } catch (err) { /* noop */ } + this.teardownVoiceTiles = null; + } + const mid = (this.selectedModelId || this.widgetWrap?.getAttribute('data-selected-model-id') || '').trim(); + const auth = (this.defaultPromptFromAuthoring ?? '').trim(); + row.replaceChildren(); + const visible = filterVoicesByModelId(all, this.voiceModelIndex, mid); + syncVoiceRowPeekClasses(row, visible.length > 4); + if (this.voicePromptInpField) { + const cur = (this.voicePromptInpField.value || '').trim(); + if (visible.length > 0 && (cur === '' || cur === auth)) { + this.voicePromptInpField.value = auth; + } else if (!visible.length) { + this.voicePromptInpField.value = ''; + } + } + if (visible.length === 0) { + this.widgetWrap?.removeAttribute('data-selected-voice-index'); + this.widgetWrap?.removeAttribute('data-selected-voice-name'); + this.widgetWrap?.removeAttribute('data-selected-voice-id'); + return; + } + const newTiles = visible.map((v, i) => buildVoiceTile(v, i, row, this)); + this.teardownVoiceTiles = attachVoiceInteractivity( + newTiles, + this, + this.voicePromptInpField, + visible, + ); + }; + + insertPromptBarAudioRoot(el, widgetInstance, widgetWrap, voiceSection, termsBanner); + const root = widgetInstance.promptBarExtendedRoot; + const initialRow = root?.querySelector('.unity-paf-voice-row') ?? null; + wireVoiceRowPeekTracking(widgetInstance, initialRow, visibleVoices.length > 4); + let removalObserver = null; + let interactivityTornDown = false; + const teardown = () => { + if (interactivityTornDown) return; + interactivityTornDown = true; + widgetInstance.clearEmptyPromptRestoreTimer?.(); + delete widgetInstance.clearEmptyPromptRestoreTimer; + removalObserver?.disconnect(); + removalObserver = null; + if (widgetInstance.teardownVoiceTiles) { + try { widgetInstance.teardownVoiceTiles(); } catch (e) { /* noop */ } + widgetInstance.teardownVoiceTiles = null; + } + if (widgetInstance.detachVoiceRowPeekTracking) { + try { widgetInstance.detachVoiceRowPeekTracking(); } catch (e) { /* noop */ } + widgetInstance.detachVoiceRowPeekTracking = null; + } + delete widgetInstance.refreshVoiceTilesForModel; + if (widgetInstance.disconnectPromptBarAudio === teardown) { + widgetInstance.disconnectPromptBarAudio = null; + } + }; + if (root) { + removalObserver = new MutationObserver(() => { + if (!root.isConnected) teardown(); + }); + removalObserver.observe(document.documentElement, { childList: true, subtree: true }); + } + widgetInstance.disconnectPromptBarAudio = teardown; +} + +export default class PromptBarAudioWidget extends UnityWidget { + constructor(...args) { + super(...args); + this.promptBarExtendedRoot = null; + this.disconnectPromptBarAudio = null; + } + + async initWidget() { + const meta = parsePromptBarAudioAuthoring(this.el); + const { + footerLink, + sectionHeading, + currentPageSourceUrl, + defaultPrompt, + exploreHtml, + termsHtml, + } = meta; + this.defaultPromptFromAuthoring = (defaultPrompt ?? '').trim(); + let voices = []; + if (currentPageSourceUrl) { + try { + voices = await loadVoicesFromCurrentPageJson(currentPageSourceUrl); + } catch (e) { + window.lana?.log(`Message: current page config json load failed, Error: ${e}`, this.lanaOptions); + } + } + this.voices = voices; + this.voiceConfigAll = voices; + this.voiceModelIndex = buildVoiceModelIndex(voices); + const { el } = this; + this.hasModelOptions = !!el.querySelector('[class*="icon-model"]'); + await mountPromptBarAudioUI(this, { + voices, + footerLink, + sectionHeading, + exploreHtml: exploreHtml || '', + termsHtml: termsHtml || '', + }); + const baseMap = this.workflowCfg.targetCfg.actionMap || {}; + return { + ...baseMap, + '.unity-paf-voice-subfoot a': { actionType: 'generate' }, + }; + } +} diff --git a/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js index e5a4144c..84a7df2d 100644 --- a/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js +++ b/unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js @@ -4,6 +4,7 @@ import { createTag, defineDeviceByScreenSize, + getUnityPromptConfigsBaseUrl, } from '../../../scripts/utils.js'; let promptWithStyleEvents = null; @@ -400,10 +401,7 @@ export class UnityWidget { } async loadModels() { - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const modelFile = `${baseUrl}/unity/configs/prompt/model-picker.json`; const results = await fetch(modelFile); if (!results.ok) { @@ -860,7 +858,7 @@ function insertPromptBarStyleRoot(el, widgetInstance, widgetWrap, styleContainer } else { el.append(root); } - widgetInstance.promptBarStyleRoot = root; + widgetInstance.promptBarExtendedRoot = root; } async function mountPromptBarStyleUI(widgetInstance, parsed) { @@ -878,7 +876,7 @@ async function mountPromptBarStyleUI(widgetInstance, parsed) { const { styleContainer, styleItems, previewArea, styleList } = createStylePreviewSection(styles, previewRows, styleSectionHeadingText); const disconnectInteractivity = attachPromptBarStyleInteractivity(styles, previewRows, inpField, styleItems, previewArea, styleList); insertPromptBarStyleRoot(el, widgetInstance, widgetWrap, styleContainer, previewArea); - const root = widgetInstance.promptBarStyleRoot; + const root = widgetInstance.promptBarExtendedRoot; let removalObserver = null; const teardownPromptBarStyle = () => { removalObserver?.disconnect(); @@ -900,7 +898,7 @@ async function mountPromptBarStyleUI(widgetInstance, parsed) { export default class PromptBarStyleWidget extends UnityWidget { constructor(...args) { super(...args); - this.promptBarStyleRoot = null; + this.promptBarExtendedRoot = null; this.disconnectPromptBarStyle = null; } diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js index f1beb176..87d94e0d 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.js @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ -import { createTag, getUnityLibs } from '../../../scripts/utils.js'; +import { createTag, getUnityLibs, getUnityPromptConfigsBaseUrl } from '../../../scripts/utils.js'; function placeholderText(root, iconClass) { const icon = root.querySelector(`.${iconClass}`) || root.querySelector(`[class*="${iconClass}"]`); @@ -227,10 +227,7 @@ export default class PromptBarUploadWidget { } async loadModels() { - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const res = await fetch(`${baseUrl}/unity/configs/prompt/model-picker-video.json`); if (!res.ok) throw new Error('Failed to fetch video models.'); const json = await res.json(); diff --git a/unitylibs/core/widgets/prompt-bar/prompt-bar.js b/unitylibs/core/widgets/prompt-bar/prompt-bar.js index df296cbd..f7a109a9 100644 --- a/unitylibs/core/widgets/prompt-bar/prompt-bar.js +++ b/unitylibs/core/widgets/prompt-bar/prompt-bar.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ -import { createTag, getConfig, unityConfig } from '../../../scripts/utils.js'; +import { createTag, getConfig, getUnityPromptConfigsBaseUrl, unityConfig } from '../../../scripts/utils.js'; export default class UnityWidget { constructor(target, el, workflowCfg, spriteCon) { @@ -516,10 +516,7 @@ export default class UnityWidget { async loadPrompts() { const { locale } = getConfig(); - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const promptFile = locale.prefix && locale.prefix !== '/' ? `${baseUrl}${locale.prefix}/unity/configs/prompt/firefly-prompt.json` : `${baseUrl}/unity/configs/prompt/firefly-prompt.json`; @@ -543,10 +540,7 @@ export default class UnityWidget { } async loadModels() { - const { origin } = window.location; - const baseUrl = (origin.includes('.aem.') || origin.includes('.hlx.')) - ? `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live` - : origin; + const baseUrl = getUnityPromptConfigsBaseUrl(); const modelFile = `${baseUrl}/unity/configs/prompt/model-picker.json`; const results = await fetch(modelFile); if (!results.ok) { diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index acb37483..82ed715c 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -35,6 +35,8 @@ export default class ActionBinder { this.unityEl = unityEl; this.workflowCfg = workflowCfg; this.block = block; + this.isPromptBarAudio = !!block?.classList?.contains('unity-prompt-bar-audio'); + this.limits = ActionBinder.resolveLimits(workflowCfg, unityEl, block); this.canvasArea = canvasArea; this.actions = actionMap; this.query = ''; @@ -69,10 +71,27 @@ export default class ActionBinder { this.verb = this.getVerbFromDom(); } + static getLimitsSuffix(unityEl, block) { + const widgetCls = [...(unityEl?.classList || [])].find((c) => c.startsWith('widget-')); + if (widgetCls) return widgetCls.replace(/^widget-/, '').trim(); + const promptBarCls = [...(block?.classList || [])].find( + (c) => c.startsWith('unity-prompt-bar-') && !c.endsWith('-host'), + ); + return promptBarCls ? promptBarCls.replace(/^unity-/, '').trim() : ''; + } + + static resolveLimits(workflowCfg, unityEl, block) { + const targetCfg = workflowCfg?.targetCfg || {}; + const commonLimits = targetCfg.limits || {}; + const widgetSuffix = ActionBinder.getLimitsSuffix(unityEl, block); + const widgetLimits = targetCfg[`limits-${widgetSuffix}`] || {}; + return { ...commonLimits, ...widgetLimits }; + } + getNetworkUtils = async () => { if (this.networkUtils) return this.networkUtils; const { default: NetworkUtils } = await import(`${getUnityLibs()}/utils/NetworkUtils.js`); - return (this.networkUtils = new NetworkUtils()); + return (this.networkUtils = new NetworkUtils()); }; showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') { @@ -234,14 +253,26 @@ export default class ActionBinder { || this.block.querySelector('.models-container .selected-model .model-name')?.textContent?.trim() || ''; + getMaxPromptCharLimit() { + const n = Number(this.limits?.['max-char-limit']); + return Number.isFinite(n) && n > 0 ? n : undefined; + } + validateInput(query) { - if (query.length > 750) { + const maxLen = this.getMaxPromptCharLimit(); + if (maxLen !== undefined && query.length > maxLen) { this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-max-length' }, 'Max prompt characters exceeded'); return { isValid: false, errorCode: 'max-prompt-characters-exceeded' }; } return { isValid: true }; } + getSelectedVoiceIdForConnector() { + if (!this.isPromptBarAudio) return undefined; + const id = this.widgetWrap.getAttribute('data-selected-voice-id')?.trim(); + return id; + } + getSelectedStylePayloadForConnector() { const root = this.block; if (!root?.classList?.contains('unity-prompt-bar-style')) return undefined; @@ -256,6 +287,11 @@ export default class ActionBinder { getSelectedStyleIndexOneBased() { const root = this.block; + if (this.isPromptBarAudio) { + const raw = this.widgetWrap.getAttribute('data-selected-voice-index'); + const idx = raw != null ? parseInt(raw, 10) : NaN; + return Number.isFinite(idx) ? idx + 1 : null; + } if (!root?.classList?.contains('unity-prompt-bar-style')) return null; const items = Array.from(root.querySelectorAll('.unity-slf-style-list .unity-slf-style-item')); const selected = root.querySelector('.unity-slf-style-item.selected'); @@ -304,7 +340,8 @@ export default class ActionBinder { } const selectedVerbType = `text-to-${currentVerb}`; const operationVerb = this.getVerbFromDom(); - const stylePayload = this.getSelectedStylePayloadForConnector(); + const stylePayload = this.isPromptBarAudio ? undefined : this.getSelectedStylePayloadForConnector(); + const voiceId = this.getSelectedVoiceIdForConnector(); const action = (this.id || !!override ? 'prompt-suggestion' : 'generate'); const styleIndexOneBased = this.getSelectedStyleIndexOneBased(); const modelName = this.getSelectedModelDisplayName(); @@ -339,6 +376,7 @@ export default class ActionBinder { ...(modelId ? { modelId } : {}), ...(modelVersion ? { modelVersion } : {}), ...(stylePayload ? { style: stylePayload } : {}), + ...(voiceId ? { voiceId } : {}), locale: getLocale(), action, }, diff --git a/unitylibs/core/workflow/workflow-firefly/sprite.svg b/unitylibs/core/workflow/workflow-firefly/sprite.svg index 868b0567..19231975 100644 --- a/unitylibs/core/workflow/workflow-firefly/sprite.svg +++ b/unitylibs/core/workflow/workflow-firefly/sprite.svg @@ -30,4 +30,17 @@ + + + + + + + + + + + + + diff --git a/unitylibs/core/workflow/workflow-firefly/target-config.json b/unitylibs/core/workflow/workflow-firefly/target-config.json index 1b63130b..0ff7f12d 100644 --- a/unitylibs/core/workflow/workflow-firefly/target-config.json +++ b/unitylibs/core/workflow/workflow-firefly/target-config.json @@ -3,6 +3,13 @@ "type": "text", "handler": "render", "renderWidget": true, + "limits": { + "max-char-limit": 750 + }, + "limits-prompt-bar-audio": { + "max-char-limit": 5000 + }, + "extendedWidgets": ["prompt-bar-style", "prompt-bar-audio"], "verbsWithoutPromptSuggestions": ["vector"], "actionMap": { ".inp-field": { "actionType": "autocomplete" }, diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js index c2328ffa..93af64e0 100644 --- a/unitylibs/core/workflow/workflow.js +++ b/unitylibs/core/workflow/workflow.js @@ -110,10 +110,10 @@ class WfInitiator { this.actionMap = this.targetConfig.actionMap; } const { default: ActionBinder } = await import(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/action-binder.js`); - const isPromptBarStyle = this.widgetName === 'prompt-bar-style'; - const styleRoot = unityWidgetObject?.promptBarStyleRoot; - const actionBinderBlock = isPromptBarStyle ? styleRoot : this.targetBlock; - const canvasAreaForBinder = isPromptBarStyle ? styleRoot : this.interactiveArea; + const isExtendedWidget = (this.targetConfig?.extendedWidgets ?? []).includes(this.widgetName); + const extendedLayoutRoot = isExtendedWidget ? (unityWidgetObject?.promptBarExtendedRoot || null) : null; + const actionBinderBlock = isExtendedWidget ? extendedLayoutRoot : this.targetBlock; + const canvasAreaForBinder = isExtendedWidget ? extendedLayoutRoot : this.interactiveArea; await new ActionBinder( this.el, this.workflowCfg, @@ -178,7 +178,7 @@ class WfInitiator { } createInteractiveArea(block, selector, targetCfg) { - if (this.widgetName === 'prompt-bar-style') return this.el; + if ((targetCfg?.extendedWidgets ?? []).includes(this.widgetName)) return this.el; const iArea = createTag('div', { class: 'interactive-area' }); const asset = block.querySelector(selector); if (asset.nodeName === 'PICTURE') { diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index 31272fcc..8e573d33 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -285,6 +285,14 @@ export function updateQueryParameter(url, paramName = 'format', oldValue = 'webp } } +export function getUnityPromptConfigsBaseUrl() { + const { origin } = window.location; + if (origin.includes('.aem.') || origin.includes('.hlx.')) { + return `https://main--unity--adobecom.${origin.includes('.hlx.') ? 'hlx' : 'aem'}.live`; + } + return origin; +} + export const unityConfig = (() => { const { host } = window.location; const commoncfg = {